Merge mozilla-central to mozilla-inbound. a=merge

This commit is contained in:
Cosmin Sabou 2018-11-21 18:25:38 +02:00
commit 6a888dd9ac
118 changed files with 3341 additions and 684 deletions

View File

@ -5479,10 +5479,19 @@ var TabContextMenu = {
let allSelectedTabsAdjacent = selectedTabs.every((element, index, array) => {
return array.length > index + 1 ? element._tPos + 1 == array[index + 1]._tPos : true;
});
contextMoveTabToEnd.disabled = selectedTabs[selectedTabs.length - 1]._tPos == gBrowser.visibleTabs.length - 1 &&
let contextTabIsSelected = this.contextTab.multiselected;
let visibleTabs = gBrowser.visibleTabs;
let lastVisibleTab = visibleTabs[visibleTabs.length - 1];
let tabsToMove = contextTabIsSelected ? selectedTabs : [this.contextTab];
let lastTabToMove = tabsToMove[tabsToMove.length - 1];
let isLastPinnedTab = lastTabToMove.pinned &&
(!lastTabToMove.nextElementSibling || !lastTabToMove.nextElementSibling.pinned);
contextMoveTabToEnd.disabled = (lastTabToMove == lastVisibleTab || isLastPinnedTab) &&
allSelectedTabsAdjacent;
let contextMoveTabToStart = document.getElementById("context_moveToStart");
contextMoveTabToStart.disabled = selectedTabs[0]._tPos == 0 && allSelectedTabsAdjacent;
let isFirstTab = tabsToMove[0] == visibleTabs[0] ||
tabsToMove[0] == visibleTabs[gBrowser._numPinnedTabs];
contextMoveTabToStart.disabled = isFirstTab && allSelectedTabsAdjacent;
// Only one of "Duplicate Tab"/"Duplicate Tabs" should be visible.
document.getElementById("context_duplicateTab").hidden = multiselectionContext;

View File

@ -32,6 +32,7 @@ support-files =
[browser_multiselect_tabs_event.js]
[browser_multiselect_tabs_move_to_another_window_drag.js]
[browser_multiselect_tabs_move_to_new_window_contextmenu.js]
[browser_multiselect_tabs_move.js]
[browser_multiselect_tabs_mute_unmute.js]
[browser_multiselect_tabs_open_related.js]
[browser_multiselect_tabs_pin_unpin.js]

View File

@ -0,0 +1,200 @@
const PREF_MULTISELECT_TABS = "browser.tabs.multiselect";
add_task(async function setPref() {
await SpecialPowers.pushPrefEnv({
set: [[PREF_MULTISELECT_TABS, true]],
});
});
add_task(async function testMoveStartEnabledClickedFromNonSelectedTab() {
let tab = gBrowser.selectedTab;
let tab2 = await addTab();
let tab3 = await addTab();
let tabs = [tab2, tab3];
let menuItemMoveStartTab = document.getElementById("context_moveToStart");
await triggerClickOn(tab, {});
await triggerClickOn(tab2, { ctrlKey: true });
ok(tab.multiselected, "Tab is multiselected");
ok(tab2.multiselected, "Tab2 is multiselected");
updateTabContextMenu(tab3);
is(menuItemMoveStartTab.disabled, false, "Move Tab to Start is enabled");
for (let tabToRemove of tabs) {
BrowserTestUtils.removeTab(tabToRemove);
}
});
add_task(async function testMoveStartDisabledFromFirstUnpinnedTab() {
let tab = gBrowser.selectedTab;
let tab2 = await addTab();
let menuItemMoveStartTab = document.getElementById("context_moveToStart");
gBrowser.pinTab(tab);
updateTabContextMenu(tab2);
is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled");
BrowserTestUtils.removeTab(tab2);
gBrowser.unpinTab(tab);
});
add_task(async function testMoveStartDisabledFromFirstPinnedTab() {
let tab = gBrowser.selectedTab;
let tab2 = await addTab();
let menuItemMoveStartTab = document.getElementById("context_moveToStart");
gBrowser.pinTab(tab);
updateTabContextMenu(tab);
is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled");
BrowserTestUtils.removeTab(tab2);
gBrowser.unpinTab(tab);
});
add_task(async function testMoveStartDisabledFromOnlyTab() {
let tab = gBrowser.selectedTab;
let menuItemMoveStartTab = document.getElementById("context_moveToStart");
updateTabContextMenu(tab);
is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled");
});
add_task(async function testMoveStartDisabledFromOnlyPinnedTab() {
let tab = gBrowser.selectedTab;
let menuItemMoveStartTab = document.getElementById("context_moveToStart");
gBrowser.pinTab(tab);
updateTabContextMenu(tab);
is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled");
gBrowser.unpinTab(tab);
});
add_task(async function testMoveStartEnabledFromLastPinnedTab() {
let tab = gBrowser.selectedTab;
let tab2 = await addTab();
let tab3 = await addTab();
let tabs = [tab2, tab3];
let menuItemMoveStartTab = document.getElementById("context_moveToStart");
gBrowser.pinTab(tab);
gBrowser.pinTab(tab2);
updateTabContextMenu(tab2);
is(menuItemMoveStartTab.disabled, false, "Move Tab to Start is enabled");
for (let tabToRemove of tabs) {
BrowserTestUtils.removeTab(tabToRemove);
}
gBrowser.unpinTab(tab);
});
add_task(async function testMoveStartDisabledFromFirstVisibleTab() {
let tab = gBrowser.selectedTab;
let tab2 = await addTab();
let menuItemMoveStartTab = document.getElementById("context_moveToStart");
gBrowser.selectTabAtIndex(1);
gBrowser.hideTab(tab);
updateTabContextMenu(tab2);
is(menuItemMoveStartTab.disabled, true, "Move Tab to Start is disabled");
BrowserTestUtils.removeTab(tab2);
gBrowser.showTab(tab);
});
add_task(async function testMoveEndEnabledClickedFromNonSelectedTab() {
let tab = gBrowser.selectedTab;
let tab2 = await addTab();
let tab3 = await addTab();
let tabs = [tab2, tab3];
let menuItemMoveEndTab = document.getElementById("context_moveToEnd");
await triggerClickOn(tab2, {});
await triggerClickOn(tab3, { ctrlKey: true });
ok(tab2.multiselected, "Tab2 is multiselected");
ok(tab3.multiselected, "Tab3 is multiselected");
updateTabContextMenu(tab);
is(menuItemMoveEndTab.disabled, false, "Move Tab to End is enabled");
for (let tabToRemove of tabs) {
BrowserTestUtils.removeTab(tabToRemove);
}
});
add_task(async function testMoveEndDisabledFromLastPinnedTab() {
let tab = gBrowser.selectedTab;
let tab2 = await addTab();
let tab3 = await addTab();
let tabs = [tab2, tab3];
let menuItemMoveEndTab = document.getElementById("context_moveToEnd");
gBrowser.pinTab(tab);
gBrowser.pinTab(tab2);
updateTabContextMenu(tab2);
is(menuItemMoveEndTab.disabled, true, "Move Tab to End is disabled");
for (let tabToRemove of tabs) {
BrowserTestUtils.removeTab(tabToRemove);
}
});
add_task(async function testMoveEndDisabledFromLastVisibleTab() {
let tab = gBrowser.selectedTab;
let tab2 = await addTab();
let menuItemMoveEndTab = document.getElementById("context_moveToEnd");
gBrowser.hideTab(tab2);
updateTabContextMenu(tab);
is(menuItemMoveEndTab.disabled, true, "Move Tab to End is disabled");
BrowserTestUtils.removeTab(tab2);
gBrowser.showTab(tab);
});
add_task(async function testMoveEndDisabledFromOnlyTab() {
let tab = gBrowser.selectedTab;
let menuItemMoveEndTab = document.getElementById("context_moveToEnd");
updateTabContextMenu(tab);
is(menuItemMoveEndTab.disabled, true, "Move Tab to End is disabled");
});
add_task(async function testMoveEndDisabledFromOnlyPinnedTab() {
let tab = gBrowser.selectedTab;
let menuItemMoveEndTab = document.getElementById("context_moveToEnd");
gBrowser.pinTab(tab);
updateTabContextMenu(tab);
is(menuItemMoveEndTab.disabled, true, "Move Tab to End is disabled");
gBrowser.unpinTab(tab);
});

View File

@ -297,7 +297,7 @@ class FormAutofillSection {
// Use case for multiple select is not considered here.
if (!option.selected) {
option.selected = true;
element.dispatchEvent(new element.ownerGlobal.UIEvent("input", {bubbles: true}));
element.dispatchEvent(new element.ownerGlobal.Event("input", {bubbles: true}));
element.dispatchEvent(new element.ownerGlobal.Event("change", {bubbles: true}));
}
// Autofill highlight appears regardless if value is changed or not

View File

@ -113,7 +113,20 @@ function triggerAutofillAndCheckProfile(profile) {
const element = document.getElementById(fieldName);
const expectingEvent = document.activeElement == element ? "DOMAutoComplete" : "change";
const checkFieldAutofilled = Promise.all([
new Promise(resolve => element.addEventListener("input", resolve, {once: true})),
new Promise(resolve => element.addEventListener("input", (event) => {
if (element.tagName == "INPUT" && element.type == "text") {
ok(event instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface on ${element.tagName}`);
} else {
ok(event instanceof Event && !(event instanceof UIEvent),
`"input" event should be dispatched with Event interface on ${element.tagName}`);
}
is(event.cancelable, false,
`"input" event should be never cancelable on ${element.tagName}`);
is(event.bubbles, true,
`"input" event should always bubble on ${element.tagName}`);
resolve();
}, {once: true})),
new Promise(resolve => element.addEventListener(expectingEvent, resolve, {once: true})),
]).then(() => checkFieldValue(element, value));

View File

@ -67,7 +67,15 @@ function checkIsFormCleared(patch = {}) {
async function confirmClear(selector) {
info("Await for clearing input");
let promise = new Promise(resolve =>
document.querySelector(selector).addEventListener("input", resolve, {once: true})
document.querySelector(selector).addEventListener("input", (event) => {
ok(event instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface');
is(event.cancelable, false,
'"input" event should be never cancelable');
is(event.bubbles, true,
'"input" event should always bubble');
resolve();
}, {once: true})
);
synthesizeKey("KEY_Enter");
await promise;

View File

@ -47,8 +47,19 @@ let MOCK_STORAGE = [{
function checkElementFilled(element, expectedvalue) {
return [
new Promise(resolve => {
element.addEventListener("input", function onInput() {
element.addEventListener("input", function onInput(event) {
ok(true, "Checking " + element.name + " field fires input event");
if (element.tagName == "INPUT" && element.type == "text") {
ok(event instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface on ${element.name}`);
} else {
ok(event instanceof Event && !(event instanceof UIEvent),
`"input" event should be dispatched with Event interface on ${element.name}`);
}
is(event.cancelable, false,
`"input" event should be never cancelable on ${element.name}`);
is(event.bubbles, true,
`"input" event should always bubble on ${element.name}`);
resolve();
}, {once: true});
}),

View File

@ -35,7 +35,7 @@ const {
RUNTIMES,
} = require("../constants");
function inspectDebugTarget({ type, id, front }) {
function inspectDebugTarget(type, id) {
return async (_, getState) => {
const runtime = getCurrentRuntime(getState().runtimes);
const { runtimeDetails, type: runtimeType } = runtime;
@ -54,9 +54,7 @@ function inspectDebugTarget({ type, id, front }) {
}
case DEBUG_TARGETS.EXTENSION: {
if (runtimeType === RUNTIMES.NETWORK || runtimeType === RUNTIMES.USB) {
// runtimeDetails.client is a ClientWrapper instance, here we need to go back
// to the actual DevTools client. Confusion should be reduce after Bug 1506056.
const devtoolsClient = runtimeDetails.client.client;
const devtoolsClient = runtimeDetails.clientWrapper.client;
await debugRemoteAddon(id, devtoolsClient);
} else if (runtimeType === RUNTIMES.THIS_FIREFOX) {
debugLocalAddon(id);
@ -65,6 +63,7 @@ function inspectDebugTarget({ type, id, front }) {
}
case DEBUG_TARGETS.WORKER: {
// Open worker toolbox in new window.
const front = runtimeDetails.client.client.getActor(id);
gDevToolsBrowser.openWorkerToolbox(front);
break;
}
@ -91,10 +90,10 @@ function installTemporaryExtension() {
function pushServiceWorker(actor) {
return async (_, getState) => {
const client = getCurrentClient(getState().runtimes);
const clientWrapper = getCurrentClient(getState().runtimes);
try {
await client.request({ to: actor, type: "push" });
await clientWrapper.request({ to: actor, type: "push" });
} catch (e) {
console.error(e);
}
@ -103,10 +102,10 @@ function pushServiceWorker(actor) {
function reloadTemporaryExtension(actor) {
return async (_, getState) => {
const client = getCurrentClient(getState().runtimes);
const clientWrapper = getCurrentClient(getState().runtimes);
try {
await client.request({ to: actor, type: "reload" });
await clientWrapper.request({ to: actor, type: "reload" });
} catch (e) {
console.error(e);
}
@ -127,10 +126,10 @@ function requestTabs() {
return async (dispatch, getState) => {
dispatch({ type: REQUEST_TABS_START });
const client = getCurrentClient(getState().runtimes);
const clientWrapper = getCurrentClient(getState().runtimes);
try {
const { tabs } = await client.listTabs({ favicons: true });
const { tabs } = await clientWrapper.listTabs({ favicons: true });
dispatch({ type: REQUEST_TABS_SUCCESS, tabs });
} catch (e) {
@ -144,10 +143,10 @@ function requestExtensions() {
dispatch({ type: REQUEST_EXTENSIONS_START });
const runtime = getCurrentRuntime(getState().runtimes);
const client = getCurrentClient(getState().runtimes);
const clientWrapper = getCurrentClient(getState().runtimes);
try {
const { addons } = await client.listAddons();
const { addons } = await clientWrapper.listAddons();
const extensions = addons.filter(a => a.debuggable);
if (runtime.type !== RUNTIMES.THIS_FIREFOX) {
// manifestURL can only be used when debugging local addons, remove this
@ -174,10 +173,14 @@ function requestWorkers() {
return async (dispatch, getState) => {
dispatch({ type: REQUEST_WORKERS_START });
const client = getCurrentClient(getState().runtimes);
const clientWrapper = getCurrentClient(getState().runtimes);
try {
const { otherWorkers, serviceWorkers, sharedWorkers } = await client.listWorkers();
const {
otherWorkers,
serviceWorkers,
sharedWorkers,
} = await clientWrapper.listWorkers();
dispatch({
type: REQUEST_WORKERS_SUCCESS,
@ -193,10 +196,10 @@ function requestWorkers() {
function startServiceWorker(actor) {
return async (_, getState) => {
const client = getCurrentClient(getState().runtimes);
const clientWrapper = getCurrentClient(getState().runtimes);
try {
await client.request({ to: actor, type: "start" });
await clientWrapper.request({ to: actor, type: "start" });
} catch (e) {
console.error(e);
}

View File

@ -38,10 +38,10 @@ const {
WATCH_RUNTIME_SUCCESS,
} = require("../constants");
async function getRuntimeInfo(runtime, client) {
async function getRuntimeInfo(runtime, clientWrapper) {
const { type } = runtime;
const { brandName: name, channel, deviceName, version } =
await client.getDeviceDescription();
await clientWrapper.getDeviceDescription();
const icon =
(channel === "release" || channel === "beta" || channel === "aurora")
? `chrome://devtools/skin/images/aboutdebugging-firefox-${ channel }.svg`
@ -68,17 +68,22 @@ function connectRuntime(id) {
dispatch({ type: CONNECT_RUNTIME_START });
try {
const runtime = findRuntimeById(id, getState().runtimes);
const { client, transportDetails } = await createClientForRuntime(runtime);
const info = await getRuntimeInfo(runtime, client);
const { clientWrapper, transportDetails } = await createClientForRuntime(runtime);
const info = await getRuntimeInfo(runtime, clientWrapper);
const promptPrefName = RUNTIME_PREFERENCE.CONNECTION_PROMPT;
const connectionPromptEnabled = await client.getPreference(promptPrefName);
const runtimeDetails = { connectionPromptEnabled, client, info, transportDetails };
const connectionPromptEnabled = await clientWrapper.getPreference(promptPrefName);
const runtimeDetails = {
clientWrapper,
connectionPromptEnabled,
info,
transportDetails,
};
if (runtime.type === RUNTIMES.USB) {
// `closed` event will be emitted when disabling remote debugging
// on the connected USB runtime.
client.addOneTimeListener("closed", onUSBDebuggerClientClosed);
clientWrapper.addOneTimeListener("closed", onUSBDebuggerClientClosed);
}
dispatch({
@ -100,13 +105,13 @@ function disconnectRuntime(id) {
dispatch({ type: DISCONNECT_RUNTIME_START });
try {
const runtime = findRuntimeById(id, getState().runtimes);
const client = runtime.runtimeDetails.client;
const { clientWrapper } = runtime.runtimeDetails;
if (runtime.type === RUNTIMES.USB) {
client.removeListener("closed", onUSBDebuggerClientClosed);
clientWrapper.removeListener("closed", onUSBDebuggerClientClosed);
}
await client.close();
await clientWrapper.close();
if (runtime.type === RUNTIMES.THIS_FIREFOX) {
DebuggerServer.destroy();
@ -130,11 +135,11 @@ function updateConnectionPromptSetting(connectionPromptEnabled) {
dispatch({ type: UPDATE_CONNECTION_PROMPT_SETTING_START });
try {
const runtime = getCurrentRuntime(getState().runtimes);
const client = runtime.runtimeDetails.client;
const { clientWrapper } = runtime.runtimeDetails;
const promptPrefName = RUNTIME_PREFERENCE.CONNECTION_PROMPT;
await client.setPreference(promptPrefName, connectionPromptEnabled);
await clientWrapper.setPreference(promptPrefName, connectionPromptEnabled);
// Re-get actual value from the runtime.
connectionPromptEnabled = await client.getPreference(promptPrefName);
connectionPromptEnabled = await clientWrapper.getPreference(promptPrefName);
dispatch({ type: UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
runtime, connectionPromptEnabled });

View File

@ -13,7 +13,7 @@ const FluentReact = require("devtools/client/shared/vendor/fluent-react");
const LocalizationProvider = createFactory(FluentReact.LocalizationProvider);
const { PAGES } = require("../constants");
const Types = require("../types");
const Types = require("../types/index");
const ConnectPage = createFactory(require("./connect/ConnectPage"));
const RuntimePage = createFactory(require("./RuntimePage"));

View File

@ -8,6 +8,8 @@ const { PureComponent } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const Types = require("../../types/index");
/**
* This component displays debug target.
*/
@ -17,7 +19,7 @@ class DebugTargetItem extends PureComponent {
actionComponent: PropTypes.any.isRequired,
detailComponent: PropTypes.any.isRequired,
dispatch: PropTypes.func.isRequired,
target: PropTypes.object.isRequired,
target: Types.debugTarget.isRequired,
};
}

View File

@ -14,6 +14,8 @@ const Localized = createFactory(FluentReact.Localized);
const DebugTargetItem = createFactory(require("./DebugTargetItem"));
const Types = require("../../types/index");
/**
* This component displays list of debug target.
*/
@ -24,7 +26,7 @@ class DebugTargetList extends PureComponent {
detailComponent: PropTypes.any.isRequired,
dispatch: PropTypes.func.isRequired,
isCollapsed: PropTypes.bool.isRequired,
targets: PropTypes.arrayOf(PropTypes.object).isRequired,
targets: PropTypes.arrayOf(Types.debugTarget).isRequired,
};
}

View File

@ -11,6 +11,7 @@ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const DebugTargetList = createFactory(require("./DebugTargetList"));
const Actions = require("../../actions/index");
const Types = require("../../types/index");
/**
* This component provides list for debug target and name area.
@ -24,7 +25,7 @@ class DebugTargetPane extends PureComponent {
dispatch: PropTypes.func.isRequired,
isCollapsed: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
targets: PropTypes.arrayOf(PropTypes.object).isRequired,
targets: PropTypes.arrayOf(Types.debugTarget).isRequired,
};
}

View File

@ -13,6 +13,8 @@ const Localized = createFactory(FluentReact.Localized);
const FieldPair = createFactory(require("./FieldPair"));
const Types = require("../../types/index");
/**
* This component displays detail information for extension.
*/
@ -21,7 +23,7 @@ class ExtensionDetail extends PureComponent {
return {
// Provided by wrapping the component with FluentReact.withLocalization.
getString: PropTypes.func.isRequired,
target: PropTypes.object.isRequired,
target: Types.debugTarget.isRequired,
};
}

View File

@ -12,6 +12,7 @@ const FluentReact = require("devtools/client/shared/vendor/fluent-react");
const Localized = createFactory(FluentReact.Localized);
const Actions = require("../../actions/index");
const Types = require("../../types/index");
/**
* This component provides inspect button.
@ -20,13 +21,13 @@ class InspectAction extends PureComponent {
static get propTypes() {
return {
dispatch: PropTypes.func.isRequired,
target: PropTypes.object.isRequired,
target: Types.debugTarget.isRequired,
};
}
inspect() {
const { dispatch, target } = this.props;
dispatch(Actions.inspectDebugTarget(target));
dispatch(Actions.inspectDebugTarget(target.type, target.id));
}
render() {

View File

@ -13,6 +13,7 @@ const FluentReact = require("devtools/client/shared/vendor/fluent-react");
const InspectAction = createFactory(require("./InspectAction"));
const Actions = require("../../actions/index");
const Types = require("../../types/index");
/**
* This component displays buttons for service worker.
@ -23,7 +24,7 @@ class ServiceWorkerAction extends PureComponent {
dispatch: PropTypes.func.isRequired,
// Provided by wrapping the component with FluentReact.withLocalization.
getString: PropTypes.func.isRequired,
target: PropTypes.object.isRequired,
target: Types.debugTarget.isRequired,
};
}

View File

@ -6,7 +6,7 @@
const { PureComponent } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const Types = require("../../types/index");
/**
* This component displays detail information for tab.
@ -14,7 +14,7 @@ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
class TabDetail extends PureComponent {
static get propTypes() {
return {
target: PropTypes.object.isRequired,
target: Types.debugTarget.isRequired,
};
}

View File

@ -14,6 +14,7 @@ const Localized = createFactory(FluentReact.Localized);
const InspectAction = createFactory(require("./InspectAction"));
const Actions = require("../../actions/index");
const Types = require("../../types/index");
/**
* This component provides components that inspect/reload/remove temporary extension.
@ -22,7 +23,7 @@ class TemporaryExtensionAction extends PureComponent {
static get propTypes() {
return {
dispatch: PropTypes.func.isRequired,
target: PropTypes.object.isRequired,
target: Types.debugTarget.isRequired,
};
}

View File

@ -17,15 +17,17 @@ const {
const FieldPair = createFactory(require("./FieldPair"));
const Types = require("../../types/index");
/**
* This component displays detail information for worker.
*/
class WorkerDetail extends PureComponent {
static get propTypes() {
return {
target: PropTypes.object.isRequired,
// Provided by wrapping the component with FluentReact.withLocalization.
getString: PropTypes.func.isRequired,
target: Types.debugTarget.isRequired,
};
}

View File

@ -12,7 +12,7 @@ const FluentReact = require("devtools/client/shared/vendor/fluent-react");
const Localized = createFactory(FluentReact.Localized);
const { PAGES, RUNTIMES } = require("../../constants");
const Types = require("../../types");
const Types = require("../../types/index");
loader.lazyRequireGetter(this, "ADB_ADDON_STATES", "devtools/shared/adb/adb-addon", true);
const SidebarItem = createFactory(require("./SidebarItem"));

View File

@ -29,43 +29,45 @@ function debugTargetListenerMiddleware(store) {
switch (action.type) {
case WATCH_RUNTIME_SUCCESS: {
const { runtime } = action;
const { client } = runtime.runtimeDetails;
const { clientWrapper } = runtime.runtimeDetails;
if (isSupportedDebugTarget(runtime.type, DEBUG_TARGETS.TAB)) {
client.addListener("tabListChanged", onTabsUpdated);
clientWrapper.addListener("tabListChanged", onTabsUpdated);
}
if (isSupportedDebugTarget(runtime.type, DEBUG_TARGETS.EXTENSION)) {
client.addListener("addonListChanged", onExtensionsUpdated);
clientWrapper.addListener("addonListChanged", onExtensionsUpdated);
}
if (isSupportedDebugTarget(runtime.type, DEBUG_TARGETS.WORKER)) {
client.addListener("workerListChanged", onWorkersUpdated);
client.addListener("serviceWorkerRegistrationListChanged", onWorkersUpdated);
client.addListener("processListChanged", onWorkersUpdated);
client.addListener("registration-changed", onWorkersUpdated);
client.addListener("push-subscription-modified", onWorkersUpdated);
clientWrapper.addListener("workerListChanged", onWorkersUpdated);
clientWrapper.addListener("serviceWorkerRegistrationListChanged",
onWorkersUpdated);
clientWrapper.addListener("processListChanged", onWorkersUpdated);
clientWrapper.addListener("registration-changed", onWorkersUpdated);
clientWrapper.addListener("push-subscription-modified", onWorkersUpdated);
}
break;
}
case UNWATCH_RUNTIME_START: {
const { runtime } = action;
const { client } = runtime.runtimeDetails;
const { clientWrapper } = runtime.runtimeDetails;
if (isSupportedDebugTarget(runtime.type, DEBUG_TARGETS.TAB)) {
client.removeListener("tabListChanged", onTabsUpdated);
clientWrapper.removeListener("tabListChanged", onTabsUpdated);
}
if (isSupportedDebugTarget(runtime.type, DEBUG_TARGETS.EXTENSION)) {
client.removeListener("addonListChanged", onExtensionsUpdated);
clientWrapper.removeListener("addonListChanged", onExtensionsUpdated);
}
if (isSupportedDebugTarget(runtime.type, DEBUG_TARGETS.WORKER)) {
client.removeListener("workerListChanged", onWorkersUpdated);
client.removeListener("serviceWorkerRegistrationListChanged", onWorkersUpdated);
client.removeListener("processListChanged", onWorkersUpdated);
client.removeListener("registration-changed", onWorkersUpdated);
client.removeListener("push-subscription-modified", onWorkersUpdated);
clientWrapper.removeListener("workerListChanged", onWorkersUpdated);
clientWrapper.removeListener("serviceWorkerRegistrationListChanged",
onWorkersUpdated);
clientWrapper.removeListener("processListChanged", onWorkersUpdated);
clientWrapper.removeListener("registration-changed", onWorkersUpdated);
clientWrapper.removeListener("push-subscription-modified", onWorkersUpdated);
}
break;
}

View File

@ -44,9 +44,14 @@ function toComponentData(workers, isServiceWorker) {
return workers.map(worker => {
// Here `worker` is the worker object created by RootFront.listAllWorkers
const type = DEBUG_TARGETS.WORKER;
const front = worker.workerTargetFront;
const icon = "chrome://devtools/skin/images/debugging-workers.svg";
let { fetch, name, registrationActor, scope } = worker;
let { fetch, name, registrationActor, scope, workerTargetFront } = worker;
// For registering service workers, workerTargetFront will not be available.
// The only valid identifier we can use at that point is the actorID for the
// service worker registration.
const id = workerTargetFront ? workerTargetFront.actorID : registrationActor;
let isActive = false;
let isRunning = false;
let status = null;
@ -60,10 +65,6 @@ function toComponentData(workers, isServiceWorker) {
}
return {
name,
icon,
front,
type,
details: {
fetch,
isActive,
@ -72,6 +73,10 @@ function toComponentData(workers, isServiceWorker) {
scope,
status,
},
icon,
id,
name,
type,
};
});
}

View File

@ -16,7 +16,7 @@ async function createLocalClient() {
DebuggerServer.registerAllActors();
const client = new DebuggerClient(DebuggerServer.connectPipe());
await client.connect();
return { client: new ClientWrapper(client) };
return { clientWrapper: new ClientWrapper(client) };
}
async function createNetworkClient(host, port) {
@ -24,7 +24,7 @@ async function createNetworkClient(host, port) {
const transport = await DebuggerClient.socketConnect(transportDetails);
const client = new DebuggerClient(transport);
await client.connect();
return { client: new ClientWrapper(client), transportDetails };
return { clientWrapper: new ClientWrapper(client), transportDetails };
}
async function createUSBClient(socketPath) {

View File

@ -12,7 +12,7 @@ exports.getCurrentRuntime = getCurrentRuntime;
function getCurrentClient(runtimesState) {
const runtimeDetails = getCurrentRuntimeDetails(runtimesState);
return runtimeDetails ? runtimeDetails.client : null;
return runtimeDetails ? runtimeDetails.clientWrapper : null;
}
exports.getCurrentClient = getCurrentClient;

View File

@ -8,11 +8,11 @@ DIRS += [
'middleware',
'modules',
'reducers',
'types',
]
DevToolsModules(
'base.css',
'constants.js',
'create-store.js',
'types.js',
)

View File

@ -0,0 +1,60 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const extensionTargetDetails = {
// actor ID for this extention.
actor: PropTypes.string.isRequired,
location: PropTypes.string.isRequired,
// manifestURL points to the manifest.json file. This URL is only valid when debugging
// local extensions so it might be null.
manifestURL: PropTypes.string,
// unique extension id.
uuid: PropTypes.string.isRequired,
};
const tabTargetDetails = {
// the url of the tab.
url: PropTypes.string.isRequired,
};
const workerTargetDetails = {
// (service worker specific) one of "LISTENING", "NOT_LISTENING". undefined otherwise.
fetch: PropTypes.string,
// (service worker specific) true if they reached the activated state.
isActive: PropTypes.bool,
// (service worker specific) true if they are currently running.
isRunning: PropTypes.bool,
// actor id for the ServiceWorkerRegistration related to this service worker.
registrationActor: PropTypes.string,
// (service worker specific) scope of the service worker registration.
scope: PropTypes.string,
// (service worker specific) one of "RUNNING", "REGISTERING", "STOPPED".
status: PropTypes.string,
};
const debugTarget = {
// details property will contain a type-specific object.
details: PropTypes.oneOfType([
PropTypes.shape(extensionTargetDetails),
PropTypes.shape(tabTargetDetails),
PropTypes.shape(workerTargetDetails),
]).isRequired,
// icon to display for the debug target.
icon: PropTypes.string.isRequired,
// unique id for the target (unique in the scope of the application lifecycle).
// - extensions: {String} extension id (for instance "someextension@mozilla.org")
// - tabs: {Number} outerWindowID
// - workers: {String} id for the WorkerTargetActor corresponding to the worker
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
// display name for the debug target.
name: PropTypes.string.isRequired,
// one of "EXTENSION", "TAB", "WORKER".
type: PropTypes.string.isRequired,
};
exports.debugTarget = PropTypes.shape(debugTarget);

View File

@ -0,0 +1,13 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { debugTarget } = require("./debug-target");
const { runtime } = require("./runtime");
module.exports = Object.assign({}, {
debugTarget,
runtime,
});

View File

@ -0,0 +1,9 @@
# 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/.
DevToolsModules(
'debug-target.js',
'index.js',
'runtime.js',
)

View File

@ -5,7 +5,7 @@
"use strict";
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const { ClientWrapper } = require("./modules/client-wrapper");
const { ClientWrapper } = require("../modules/client-wrapper");
const runtimeInfo = {
// device name which is running the runtime,
@ -32,7 +32,7 @@ const runtimeTransportDetails = {
const runtimeDetails = {
// ClientWrapper built using a DebuggerClient for the runtime
client: PropTypes.instanceOf(ClientWrapper).isRequired,
clientWrapper: PropTypes.instanceOf(ClientWrapper).isRequired,
// reflect devtools.debugger.prompt-connection preference of this runtime
connectionPromptEnabled: PropTypes.bool.isRequired,

View File

@ -35,7 +35,7 @@ class UsbMocks {
this.runtimeClientFactoryMock = createRuntimeClientFactoryMock();
this._clients = {};
this.runtimeClientFactoryMock.createClientForRuntime = runtime => {
return { client: this._clients[runtime.id] };
return { clientWrapper: this._clients[runtime.id] };
};
// Add a client for THIS_FIREFOX, since about:debugging will start on the This Firefox

View File

@ -266,7 +266,9 @@ const reducers = {
return addDecl.index === decl.index;
});
if (rule.remove[removeIndex] && rule.remove[removeIndex].value === decl.value) {
if (rule.remove[removeIndex] &&
rule.remove[removeIndex].value === decl.value &&
rule.remove[removeIndex].property === decl.property) {
// Delete any previous remove operation which would be canceled out by this add.
rule.remove.splice(removeIndex, 1);
} else if (rule.add[addIndex]) {

View File

@ -17,4 +17,5 @@ support-files =
[browser_changes_declaration_edit_value.js]
[browser_changes_declaration_remove_ahead.js]
[browser_changes_declaration_remove.js]
[browser_changes_declaration_rename.js]
[browser_changes_rule_selector.js]

View File

@ -19,7 +19,6 @@ add_task(async function() {
await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
const { inspector, view: ruleView } = await openRuleView();
const { document: doc, store } = selectChangesView(inspector);
const panel = doc.querySelector("#sidebar-panel-changes");
await selectNode("div", inspector);
const rule = getRuleViewRuleEditor(ruleView, 1).rule;
@ -31,7 +30,7 @@ add_task(async function() {
info("Wait for change to be tracked");
await onTrackChange;
let removedDeclarations = panel.querySelectorAll(".diff-remove");
let removedDeclarations = getRemovedDeclarations(doc);
is(removedDeclarations.length, 1, "Only one declaration was tracked as removed");
onTrackChange = waitUntilAction(store, "TRACK_CHANGE");
@ -40,8 +39,8 @@ add_task(async function() {
info("Wait for change to be tracked");
await onTrackChange;
const addedDeclarations = panel.querySelectorAll(".diff-add");
removedDeclarations = panel.querySelectorAll(".diff-remove");
const addedDeclarations = getAddedDeclarations(doc);
removedDeclarations = getRemovedDeclarations(doc);
is(addedDeclarations.length, 0, "No declarations were tracked as added");
is(removedDeclarations.length, 0, "No declarations were tracked as removed");
});

View File

@ -18,15 +18,14 @@ add_task(async function() {
await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
const { inspector, view: ruleView } = await openRuleView();
const { document: doc, store } = selectChangesView(inspector);
const panel = doc.querySelector("#sidebar-panel-changes");
await selectNode("div", inspector);
await testAddDuplicateDeclarations(ruleView, store, panel);
await testChangeDuplicateDeclarations(ruleView, store, panel);
await testRemoveDuplicateDeclarations(ruleView, store, panel);
await testAddDuplicateDeclarations(ruleView, store, doc);
await testChangeDuplicateDeclarations(ruleView, store, doc);
await testRemoveDuplicateDeclarations(ruleView, store, doc);
});
async function testAddDuplicateDeclarations(ruleView, store, panel) {
async function testAddDuplicateDeclarations(ruleView, store, doc) {
info(`Test that adding declarations with the same property name and value
are both tracked.`);
@ -42,18 +41,17 @@ async function testAddDuplicateDeclarations(ruleView, store, panel) {
info("Wait for the change to be tracked");
await onTrackChange;
const addDecl = panel.querySelectorAll(".declaration.diff-add");
const addDecl = getAddedDeclarations(doc);
is(addDecl.length, 2, "Two declarations were tracked as added");
is(addDecl.item(0).querySelector(".declaration-value").textContent, "red",
is(addDecl[0].value, "red",
"First declaration has correct property value"
);
is(addDecl.item(0).querySelector(".declaration-value").textContent,
addDecl.item(1).querySelector(".declaration-value").textContent,
is(addDecl[0].value, addDecl[1].value,
"First and second declarations have identical property values"
);
}
async function testChangeDuplicateDeclarations(ruleView, store, panel) {
async function testChangeDuplicateDeclarations(ruleView, store, doc) {
info("Test that changing one of the duplicate declarations won't change the other");
const rule = getRuleViewRuleEditor(ruleView, 1).rule;
const prop = rule.textProps[0];
@ -64,16 +62,12 @@ async function testChangeDuplicateDeclarations(ruleView, store, panel) {
info("Wait for the change to be tracked");
await onTrackChange;
const addDecl = panel.querySelectorAll(".declaration.diff-add");
is(addDecl.item(0).querySelector(".declaration-value").textContent, "black",
"First declaration has changed property value"
);
is(addDecl.item(1).querySelector(".declaration-value").textContent, "red",
"Second declaration has not changed property value"
);
const addDecl = getAddedDeclarations(doc);
is(addDecl[0].value, "black", "First declaration has changed property value");
is(addDecl[1].value, "red", "Second declaration has not changed property value");
}
async function testRemoveDuplicateDeclarations(ruleView, store, panel) {
async function testRemoveDuplicateDeclarations(ruleView, store, doc) {
info(`Test that removing the first of the duplicate declarations
will not remove the second.`);
const rule = getRuleViewRuleEditor(ruleView, 1).rule;
@ -85,12 +79,12 @@ async function testRemoveDuplicateDeclarations(ruleView, store, panel) {
info("Wait for the change to be tracked");
await onTrackChange;
const addDecl = panel.querySelectorAll(".declaration.diff-add");
const removeDecl = panel.querySelectorAll(".declaration.diff-remove");
const addDecl = getAddedDeclarations(doc);
const removeDecl = getRemovedDeclarations(doc);
// Expect no remove operation tracked because it cancels out the original add operation.
is(removeDecl.length, 0, "No declaration was tracked as removed");
is(addDecl.length, 1, "Just one declaration left tracked as added");
is(addDecl.item(0).querySelector(".declaration-value").textContent, "red",
is(addDecl[0].value, "red",
"Leftover declaration has property value of the former second declaration"
);
}

View File

@ -53,15 +53,14 @@ add_task(async function() {
await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
const { inspector, view: ruleView } = await openRuleView();
const { document: doc, store } = selectChangesView(inspector);
const panel = doc.querySelector("#sidebar-panel-changes");
await selectNode("div", inspector);
const rule = getRuleViewRuleEditor(ruleView, 1).rule;
const prop = rule.textProps[0];
let onTrackChange;
let removeValue;
let addValue;
let removeDecl;
let addDecl;
for (const { value, add, remove } of VALUE_CHANGE_ITERATIONS) {
onTrackChange = waitUntilAction(store, "TRACK_CHANGE");
@ -71,25 +70,24 @@ add_task(async function() {
info("Wait for the change to be tracked");
await onTrackChange;
addValue = panel.querySelector(".diff-add .declaration-value");
removeValue = panel.querySelector(".diff-remove .declaration-value");
addDecl = getAddedDeclarations(doc);
removeDecl = getRemovedDeclarations(doc);
if (add) {
is(addValue.textContent, add.value,
is(addDecl[0].value, add.value,
`Added declaration has expected value: ${add.value}`);
is(panel.querySelectorAll(".diff-add").length, 1,
"Only one declaration was tracked as added.");
is(addDecl.length, 1, "Only one declaration was tracked as added.");
} else {
ok(!addValue, `Added declaration was cleared`);
is(addDecl.length, 0, "Added declaration was cleared");
}
if (remove) {
is(removeValue.textContent, remove.value,
is(removeDecl[0].value, remove.value,
`Removed declaration has expected value: ${remove.value}`);
is(panel.querySelectorAll(".diff-remove").length, 1,
is(removeDecl.length, 1,
"Only one declaration was tracked as removed.");
} else {
ok(!removeValue, `Removed declaration was cleared`);
is(removeDecl.length, 0, "Removed declaration was cleared");
}
}
});

View File

@ -19,7 +19,6 @@ add_task(async function() {
await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
const { inspector, view: ruleView } = await openRuleView();
const { document: doc, store } = selectChangesView(inspector);
const panel = doc.querySelector("#sidebar-panel-changes");
await selectNode("div", inspector);
const rule = getRuleViewRuleEditor(ruleView, 1).rule;
@ -31,8 +30,8 @@ add_task(async function() {
info("Wait for change to be tracked");
await onTrackChange;
const removeName = panel.querySelector(".diff-remove .declaration-name");
const removeValue = panel.querySelector(".diff-remove .declaration-value");
is(removeName.textContent, "color", "Correct declaration name was tracked as removed");
is(removeValue.textContent, "red", "Correct declaration value was tracked as removed");
const removeDecl = getRemovedDeclarations(doc);
is(removeDecl.length, 1, "One declaration was tracked as removed");
is(removeDecl[0].property, "color", "Correct declaration name was tracked as removed");
is(removeDecl[0].value, "red", "Correct declaration value was tracked as removed");
});

View File

@ -21,7 +21,6 @@ add_task(async function() {
await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
const { inspector, view: ruleView } = await openRuleView();
const { document: doc, store } = selectChangesView(inspector);
const panel = doc.querySelector("#sidebar-panel-changes");
await selectNode("div", inspector);
const rule = getRuleViewRuleEditor(ruleView, 1).rule;
@ -44,16 +43,12 @@ add_task(async function() {
info("Wait for change to be tracked");
await onTrackChange;
const removeDecl = panel.querySelectorAll(".declaration.diff-remove");
const addDecl = panel.querySelectorAll(".declaration.diff-add");
const removeDecl = getRemovedDeclarations(doc);
const addDecl = getAddedDeclarations(doc);
is(removeDecl.length, 2, "Two declarations tracked as removed");
is(addDecl.length, 1, "One declaration tracked as added");
// Ensure changes to the second declaration were tracked after removing the first one.
is(addDecl.item(0).querySelector(".declaration-name").textContent, "display",
"Added declaration has updated property name"
);
is(addDecl.item(0).querySelector(".declaration-value").textContent, "flex",
"Added declaration has updated property value"
);
is(addDecl[0].property, "display", "Added declaration has updated property name");
is(addDecl[0].value, "flex", "Added declaration has updated property value");
});

View File

@ -0,0 +1,61 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that renaming the property of a CSS declaration in the Rule view is tracked.
const TEST_URI = `
<style type='text/css'>
div {
color: red;
}
</style>
<div></div>
`;
add_task(async function() {
await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
const { inspector, view: ruleView } = await openRuleView();
const { document: doc, store } = selectChangesView(inspector);
await selectNode("div", inspector);
const rule = getRuleViewRuleEditor(ruleView, 1).rule;
const prop = rule.textProps[0];
let onTrackChange;
const oldPropertyName = "color";
const newPropertyName = "background-color";
info(`Rename the CSS declaration name to ${newPropertyName}`);
onTrackChange = waitUntilAction(store, "TRACK_CHANGE");
await renameProperty(ruleView, prop, newPropertyName);
info("Wait for the change to be tracked");
await onTrackChange;
let removeDecl = getRemovedDeclarations(doc);
let addDecl = getAddedDeclarations(doc);
is(removeDecl.length, 1, "One declaration tracked as removed");
is(removeDecl[0].property, oldPropertyName,
`Removed declaration has old property name: ${oldPropertyName}`
);
is(addDecl.length, 1, "One declaration tracked as added");
is(addDecl[0].property, newPropertyName,
`Added declaration has new property name: ${newPropertyName}`
);
info(`Reverting the CSS declaration name to ${oldPropertyName} should clear changes.`);
onTrackChange = waitUntilAction(store, "TRACK_CHANGE");
await renameProperty(ruleView, prop, oldPropertyName);
info("Wait for the change to be tracked");
await onTrackChange;
removeDecl = getRemovedDeclarations(doc);
addDecl = getAddedDeclarations(doc);
is(removeDecl.length, 0, "No declaration tracked as removed");
is(addDecl.length, 0, "No declaration tracked as added");
});

View File

@ -29,3 +29,34 @@ registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.inspector.changes.enabled");
Services.prefs.clearUserPref("devtools.inspector.three-pane-enabled");
});
/**
* Get an array of objects with property/value pairs of the CSS declarations rendered
* in the Changes panel.
*
* @param {Document} panelDoc
* Host document of the Changes panel.
* @param {String} selector
* Optional selector to filter rendered declaration DOM elements.
* One of ".diff-remove" or ".diff-add".
* If omitted, all declarations will be returned.
* @return {Array}
*/
function getDeclarations(panelDoc, selector = "") {
const els = panelDoc.querySelectorAll(`#sidebar-panel-changes .declaration${selector}`);
return [...els].map(el => {
return {
property: el.querySelector(".declaration-name").textContent,
value: el.querySelector(".declaration-value").textContent,
};
});
}
function getAddedDeclarations(panelDoc) {
return getDeclarations(panelDoc, ".diff-add");
}
function getRemovedDeclarations(panelDoc) {
return getDeclarations(panelDoc, ".diff-remove");
}

View File

@ -366,6 +366,34 @@ var setProperty = async function(view, textProp, value,
}
};
/**
* Change the name of a property in a rule in the rule-view.
*
* @param {CssRuleView} view
* The instance of the rule-view panel.
* @param {TextProperty} textProp
* The instance of the TextProperty to be changed.
* @param {String} name
* The new property name.
*/
var renameProperty = async function(view, textProp, name) {
await focusEditableField(view, textProp.editor.nameSpan);
const onNameDone = view.once("ruleview-changed");
info(`Rename the property to ${name}`);
EventUtils.sendString(name, view.styleWindow);
EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
info("Wait for property name.");
await onNameDone;
// Renaming the property auto-advances the focus to the value input. Exiting without
// committing will still fire a change event. @see TextPropertyEditor._onValueDone().
// Wait for that event too before proceeding.
const onValueDone = view.once("ruleview-changed");
EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
info("Wait for property value.");
await onValueDone;
};
/**
* Simulate removing a property from an existing rule in the rule-view.
*

View File

@ -98,7 +98,9 @@ public:
// being connected.
void Unbind();
// Only intended for UA widgets / special shadow roots.
// Only intended for UA widgets / special shadow roots, or for handling
// failure cases when adopting (see BlastSubtreeToPieces).
//
// Forgets our shadow host and unbinds all our kids.
void Unattach();

View File

@ -0,0 +1,17 @@
<script>
window.onload=function(){
let bigShadowTree = `<div>`;
for (let i = 0; i < 10; ++i)
bigShadowTree = bigShadowTree + bigShadowTree;
b.attachShadow({ mode: 'open' }).innerHTML = bigShadowTree;
// Create wrappers for all those elements.
[...b.shadowRoot.querySelectorAll('*')].forEach(() => {});
document.documentElement.addEventListener('DOMNodeRemoved', function(){
window.frames[0].document.body.appendChild(b)
})
window.frames[0].document.body.appendChild(a)
}
</script>
<mark id='a'>
<iframe></iframe>
<div id='b'>

View File

@ -245,3 +245,4 @@ load 1459688.html
load 1460794.html
load 1505875.html
load 1505811.html
load 1508845.html

View File

@ -57,6 +57,7 @@
#include "mozilla/dom/HTMLInputElement.h"
#include "mozilla/dom/HTMLSlotElement.h"
#include "mozilla/dom/HTMLTemplateElement.h"
#include "mozilla/dom/HTMLTextAreaElement.h"
#include "mozilla/dom/IDTracker.h"
#include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/dom/KeyboardEventBinding.h"
@ -85,6 +86,7 @@
#include "mozilla/dom/Selection.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs.h"
#include "mozilla/TextEditor.h"
#include "mozilla/TextEvents.h"
#include "nsArrayUtils.h"
#include "nsAString.h"
@ -4367,6 +4369,8 @@ nsContentUtils::DispatchTrustedEvent(nsIDocument* aDoc, nsISupports* aTarget,
Composed aComposed,
bool* aDefaultAction)
{
MOZ_ASSERT(!aEventName.EqualsLiteral("input"),
"Use DispatchInputEvent() instead");
return DispatchEvent(aDoc, aTarget, aEventName, aCanBubble, aCancelable,
aComposed, Trusted::eYes, aDefaultAction);
}
@ -4450,6 +4454,110 @@ nsContentUtils::DispatchEvent(nsIDocument* aDoc, nsISupports* aTarget,
return rv;
}
// static
nsresult
nsContentUtils::DispatchInputEvent(Element* aEventTargetElement)
{
RefPtr<TextEditor> textEditor; // See bug 1506439
return DispatchInputEvent(aEventTargetElement, textEditor);
}
// static
nsresult
nsContentUtils::DispatchInputEvent(Element* aEventTargetElement,
TextEditor* aTextEditor)
{
if (NS_WARN_IF(!aEventTargetElement)) {
return NS_ERROR_INVALID_ARG;
}
// If this is called from editor, the instance should be set to aTextEditor.
// Otherwise, we need to look for an editor for aEventTargetElement.
// However, we don't need to do it for HTMLEditor since nobody shouldn't
// dispatch "input" event for HTMLEditor except HTMLEditor itself.
bool useInputEvent = false;
if (aTextEditor) {
useInputEvent = true;
} else if (HTMLTextAreaElement* textAreaElement=
HTMLTextAreaElement::FromNode(aEventTargetElement)) {
aTextEditor = textAreaElement->GetTextEditorWithoutCreation();
useInputEvent = true;
} else if (HTMLInputElement* inputElement =
HTMLInputElement::FromNode(aEventTargetElement)) {
if (inputElement->IsInputEventTarget()) {
aTextEditor = inputElement->GetTextEditorWithoutCreation();
useInputEvent = true;
}
}
#ifdef DEBUG
else {
nsCOMPtr<nsITextControlElement> textControlElement =
do_QueryInterface(aEventTargetElement);
MOZ_ASSERT(!textControlElement,
"The event target may have editor, but we've not known it yet.");
}
#endif // #ifdef DEBUG
if (!useInputEvent) {
// Dispatch "input" event with Event instance.
WidgetEvent widgetEvent(true, eUnidentifiedEvent);
widgetEvent.mSpecifiedEventType = nsGkAtoms::oninput;
widgetEvent.mFlags.mCancelable = false;
// Using same time as nsContentUtils::DispatchEvent() for backward
// compatibility.
widgetEvent.mTime = PR_Now();
(new AsyncEventDispatcher(aEventTargetElement,
widgetEvent))->RunDOMEventWhenSafe();
return NS_OK;
}
nsCOMPtr<nsIWidget> widget;
if (aTextEditor) {
widget = aTextEditor->GetWidget();
if (NS_WARN_IF(!widget)) {
return NS_ERROR_FAILURE;
}
} else {
nsIDocument* document = aEventTargetElement->OwnerDoc();
if (NS_WARN_IF(!document)) {
return NS_ERROR_FAILURE;
}
// If we're running xpcshell tests, we fail to get presShell here.
// Even in such case, we need to dispatch "input" event without widget.
nsIPresShell* presShell = document->GetShell();
if (presShell) {
nsPresContext* presContext = presShell->GetPresContext();
if (NS_WARN_IF(!presContext)) {
return NS_ERROR_FAILURE;
}
widget = presContext->GetRootWidget();
if (NS_WARN_IF(!widget)) {
return NS_ERROR_FAILURE;
}
}
}
// Dispatch "input" event with InputEvent instance.
InternalEditorInputEvent inputEvent(true, eEditorInput, widget);
// Using same time as old event dispatcher in EditorBase for backward
// compatibility.
inputEvent.mTime = static_cast<uint64_t>(PR_Now() / 1000);
// If there is an editor, set isComposing to true when it has composition.
// Note that EditorBase::IsIMEComposing() may return false even when we
// need to set it to true.
// Otherwise, i.e., editor hasn't been created for the element yet,
// we should set isComposing to false since the element can never has
// composition without editor.
inputEvent.mIsComposing =
aTextEditor ? !!aTextEditor->GetComposition() : false;
(new AsyncEventDispatcher(aEventTargetElement,
inputEvent))->RunDOMEventWhenSafe();
return NS_OK;
}
nsresult
nsContentUtils::DispatchChromeEvent(nsIDocument *aDoc,
nsISupports *aTarget,

View File

@ -123,6 +123,7 @@ class Dispatcher;
class ErrorResult;
class EventListenerManager;
class HTMLEditor;
class TextEditor;
namespace dom {
class ContentFrameMessageManager;
@ -1377,9 +1378,12 @@ public:
static void MaybeFireNodeRemoved(nsINode* aChild, nsINode* aParent);
/**
* This method creates and dispatches a trusted event.
* These methods create and dispatch a trusted event.
* Works only with events which can be created by calling
* nsIDocument::CreateEvent() with parameter "Events".
* Note that don't use these methods for "input" event. Use
* DispatchInputEvent() instead.
*
* @param aDoc The document which will be used to create the event.
* @param aTarget The target of the event, should be QIable to
* EventTarget.
@ -1438,6 +1442,26 @@ public:
aDefaultAction, aOnlyChromeDispatch);
}
/**
* This method dispatches "input" event with proper event class. If it's
* unsafe to dispatch, this put the event into the script runner queue.
* Input Events spec defines as:
* Input events are dispatched on elements that act as editing hosts,
* including elements with the contenteditable attribute set, textarea
* elements, and input elements that permit text input.
*
* @param aEventTarget The event target element of the "input" event.
* Must not be nullptr.
* @param aTextEditor Optional. If this is called by editor,
* editor should set this. Otherwise, leave
* nullptr.
*/
MOZ_CAN_RUN_SCRIPT
static nsresult DispatchInputEvent(Element* aEventTarget);
MOZ_CAN_RUN_SCRIPT
static nsresult DispatchInputEvent(Element* aEventTarget,
mozilla::TextEditor* aTextEditor);
/**
* This method creates and dispatches a untrusted event.
* Works only with events which can be created by calling

View File

@ -7048,6 +7048,11 @@ nsDOMAttributeMap::BlastSubtreeToPieces(nsINode* aNode)
NS_ASSERTION(NS_SUCCEEDED(rv), "Uh-oh, UnsetAttr shouldn't fail!");
}
}
if (ShadowRoot* shadow = element->GetShadowRoot()) {
BlastSubtreeToPieces(shadow);
element->UnattachShadow();
}
}
while (aNode->HasChildren()) {

View File

@ -59,6 +59,16 @@ function onunload()
SimpleTest.finish();
}
function checkInputEvent(aEvent, aIsComposing, aDescription) {
if (aEvent.type != "input") {
return;
}
ok(aEvent instanceof InputEvent, `${aDescription}"input" event should be dispatched with InputEvent interface`);
is(aEvent.cancelable, false, `${aDescription}"input" event should be never cancelable`);
is(aEvent.bubbles, true, `${aDescription}"input" event should always bubble`);
is(aEvent.isComposing, aIsComposing, `${aDescription}isComposing should be ${aIsComposing}`);
}
const kIsMac = (navigator.platform.indexOf("Mac") == 0);
var iframe = document.getElementById("iframe");
@ -222,6 +232,7 @@ function runBeginInputTransactionMethodTests()
description + "events[2] should be text");
is(events[3].type, "input",
description + "events[3] should be input");
checkInputEvent(events[3], true, description);
TIP1.cancelComposition();
// Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitComposition().
@ -257,6 +268,7 @@ function runBeginInputTransactionMethodTests()
description + "events[1] should be compositionend");
is(events[2].type, "input",
description + "events[2] should be input");
checkInputEvent(events[2], false, description);
// Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitCompositionWith("bar").
events = [];
@ -304,6 +316,7 @@ function runBeginInputTransactionMethodTests()
description + "events[3] should be compositionend");
is(events[4].type, "input",
description + "events[4] should be input");
checkInputEvent(events[4], false, description);
// Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during cancelComposition().
events = [];
@ -346,6 +359,7 @@ function runBeginInputTransactionMethodTests()
description + "events[2] should be compositionend");
is(events[3].type, "input",
description + "events[3] should be input");
checkInputEvent(events[3], false, description);
// Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during keydown() and keyup().
events = [];
@ -385,6 +399,7 @@ function runBeginInputTransactionMethodTests()
description + "events[1] should be keypress");
is(events[2].type, "input",
description + "events[2] should be input");
checkInputEvent(events[2], false, description);
is(events[3].type, "keyup",
description + "events[3] should be keyup");
@ -442,6 +457,7 @@ function runBeginInputTransactionMethodTests()
description + "events[2] should be text");
is(events[3].type, "input",
description + "events[3] should be input");
checkInputEvent(events[3], true, description);
TIP1.cancelComposition();
// Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitComposition().
@ -477,6 +493,7 @@ function runBeginInputTransactionMethodTests()
description + "events[1] should be compositionend");
is(events[2].type, "input",
description + "events[2] should be input");
checkInputEvent(events[2], false, description);
// Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitCompositionWith("bar").
events = [];
@ -524,6 +541,7 @@ function runBeginInputTransactionMethodTests()
description + "events[3] should be compositionend");
is(events[4].type, "input",
description + "events[4] should be input");
checkInputEvent(events[4], false, description);
// Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during cancelComposition().
events = [];
@ -566,6 +584,7 @@ function runBeginInputTransactionMethodTests()
description + "events[2] should be compositionend");
is(events[3].type, "input",
description + "events[3] should be input");
checkInputEvent(events[3], false, description);
// Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during keydown() and keyup().
events = [];
@ -605,6 +624,7 @@ function runBeginInputTransactionMethodTests()
description + "events[1] should be keypress");
is(events[2].type, "input",
description + "events[2] should be input");
checkInputEvent(events[2], false, description);
is(events[3].type, "keyup",
description + "events[3] should be keyup");
@ -692,6 +712,7 @@ function runBeginInputTransactionMethodTests()
description + "events[2] should be text");
is(events[3].type, "input",
description + "events[3] should be input");
checkInputEvent(events[3], true, description);
TIP1.cancelComposition();
// Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during commitComposition().
@ -745,6 +766,7 @@ function runBeginInputTransactionMethodTests()
description + "events[1] should be compositionend");
is(events[2].type, "input",
description + "events[2] should be input");
checkInputEvent(events[2], false, description);
// Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during commitCompositionWith("bar");.
events = [];
@ -822,6 +844,7 @@ function runBeginInputTransactionMethodTests()
description + "events[3] should be compositionend");
is(events[4].type, "input",
description + "events[4] should be input");
checkInputEvent(events[4], false, description);
// Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during cancelComposition();.
events = [];
@ -888,6 +911,7 @@ function runBeginInputTransactionMethodTests()
description + "events[2] should be compositionend");
is(events[3].type, "input",
description + "events[3] should be input");
checkInputEvent(events[3], false, description);
// Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during keydown() and keyup();.
events = [];
@ -951,6 +975,7 @@ function runBeginInputTransactionMethodTests()
description + "events[1] should be keypress");
is(events[2].type, "input",
description + "events[2] should be input");
checkInputEvent(events[2], false, description);
is(events[3].type, "keyup",
description + "events[3] should be keyup");
@ -1038,6 +1063,7 @@ function runBeginInputTransactionMethodTests()
description + "events[2] should be text");
is(events[3].type, "input",
description + "events[3] should be input");
checkInputEvent(events[3], true, description);
TIP1.cancelComposition();
// Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during commitComposition().
@ -1091,6 +1117,7 @@ function runBeginInputTransactionMethodTests()
description + "events[1] should be compositionend");
is(events[2].type, "input",
description + "events[2] should be input");
checkInputEvent(events[2], false, description);
// Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during commitCompositionWith("bar");.
events = [];
@ -1168,6 +1195,7 @@ function runBeginInputTransactionMethodTests()
description + "events[3] should be compositionend");
is(events[4].type, "input",
description + "events[4] should be input");
checkInputEvent(events[4], false, description);
// Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during cancelComposition();.
events = [];
@ -1234,6 +1262,7 @@ function runBeginInputTransactionMethodTests()
description + "events[2] should be compositionend");
is(events[3].type, "input",
description + "events[3] should be input");
checkInputEvent(events[3], false, description);
// Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during keydown() and keyup();.
events = [];
@ -1297,6 +1326,7 @@ function runBeginInputTransactionMethodTests()
description + "events[1] should be keypress");
is(events[2].type, "input",
description + "events[2] should be input");
checkInputEvent(events[2], false, description);
is(events[3].type, "keyup",
description + "events[3] should be keyup");

View File

@ -2328,6 +2328,9 @@ WebGLContext::GetVRFrame()
already_AddRefed<layers::SharedSurfaceTextureClient>
WebGLContext::GetVRFrame()
{
if (!gl)
return nullptr;
EnsureVRReady();
/**
* Swap buffers as though composition has occurred.
@ -2337,9 +2340,6 @@ WebGLContext::GetVRFrame()
BeginComposition();
EndComposition();
if (!gl)
return nullptr;
gl::GLScreenBuffer* screen = gl->Screen();
if (!screen)
return nullptr;

View File

@ -251,16 +251,12 @@ public:
Unused << NS_WARN_IF(NS_FAILED(DispatchEvents()));
}
MOZ_CAN_RUN_SCRIPT_BOUNDARY
nsresult
DispatchEvents()
{
nsresult rv = NS_OK;
rv = nsContentUtils::DispatchTrustedEvent(mInputElement->OwnerDoc(),
static_cast<Element*>(mInputElement.get()),
NS_LITERAL_STRING("input"),
CanBubble::eYes,
Cancelable::eNo);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "DispatchTrustedEvent failed");
nsresult rv = nsContentUtils::DispatchInputEvent(mInputElement);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to dispatch input event");
rv = nsContentUtils::DispatchTrustedEvent(mInputElement->OwnerDoc(),
static_cast<Element*>(mInputElement.get()),
@ -595,7 +591,9 @@ public:
NS_DECL_ISUPPORTS
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD Update(const nsAString& aColor) override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD Done(const nsAString& aColor) override;
private:
@ -604,6 +602,7 @@ private:
* If aTrustedUpdate is true, it will consider that aColor is a new value.
* Otherwise, it will check that aColor is different from the current value.
*/
MOZ_CAN_RUN_SCRIPT
nsresult UpdateInternal(const nsAString& aColor, bool aTrustedUpdate);
RefPtr<HTMLInputElement> mInput;
@ -634,15 +633,14 @@ nsColorPickerShownCallback::UpdateInternal(const nsAString& aColor,
}
}
if (valueChanged) {
mValueChanged = true;
return nsContentUtils::DispatchTrustedEvent(mInput->OwnerDoc(),
static_cast<Element*>(mInput.get()),
NS_LITERAL_STRING("input"),
CanBubble::eYes,
Cancelable::eNo);
if (!valueChanged) {
return NS_OK;
}
mValueChanged = true;
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(mInput);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch input event");
return NS_OK;
}
@ -2346,6 +2344,10 @@ HTMLInputElement::SetUserInput(const nsAString& aValue,
return;
}
bool isInputEventDispatchedByTextEditorState =
GetValueMode() == VALUE_MODE_VALUE &&
IsSingleLineTextControl(false);
nsresult rv =
SetValueInternal(aValue,
nsTextEditorState::eSetValue_BySetUserInput |
@ -2353,13 +2355,11 @@ HTMLInputElement::SetUserInput(const nsAString& aValue,
nsTextEditorState::eSetValue_MoveCursorToEndIfValueChanged);
NS_ENSURE_SUCCESS_VOID(rv);
// FIXME: We're inconsistent about whether "input" events are cancelable or
// not.
nsContentUtils::DispatchTrustedEvent(OwnerDoc(),
static_cast<Element*>(this),
NS_LITERAL_STRING("input"),
CanBubble::eYes,
Cancelable::eYes);
if (!isInputEventDispatchedByTextEditorState) {
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch input event");
}
// If this element is not currently focused, it won't receive a change event for this
// update through the normal channels. So fire a change event immediately, instead.
@ -2390,6 +2390,16 @@ HTMLInputElement::GetTextEditor()
return GetTextEditorFromState();
}
NS_IMETHODIMP_(TextEditor*)
HTMLInputElement::GetTextEditorWithoutCreation()
{
nsTextEditorState* state = GetEditorState();
if (!state) {
return nullptr;
}
return state->GetTextEditorWithoutCreation();
}
NS_IMETHODIMP_(nsISelectionController*)
HTMLInputElement::GetSelectionController()
{
@ -2735,7 +2745,7 @@ HTMLInputElement::SetFiles(FileList* aFiles)
/* static */ void
HTMLInputElement::HandleNumberControlSpin(void* aData)
{
HTMLInputElement* input = static_cast<HTMLInputElement*>(aData);
RefPtr<HTMLInputElement> input = static_cast<HTMLInputElement*>(aData);
NS_ASSERTION(input->mNumberControlSpinnerIsSpinning,
"Should have called nsRepeatService::Stop()");
@ -2806,6 +2816,11 @@ HTMLInputElement::SetValueInternal(const nsAString& aValue,
}
if (IsSingleLineTextControl(false)) {
// Note that if aFlags includes
// nsTextEditorState::eSetValue_BySetUserInput, "input" event is
// automatically dispatched by nsTextEditorState::SetValue().
// If you'd change condition of calling this method, you need to
// maintain SetUserInput() too.
if (!mInputData.mState->SetValue(value, aOldValue, aFlags)) {
return NS_ERROR_OUT_OF_MEMORY;
}
@ -3750,12 +3765,9 @@ HTMLInputElement::CancelRangeThumbDrag(bool aIsForUserEvent)
if (frame) {
frame->UpdateForValueChange();
}
RefPtr<AsyncEventDispatcher> asyncDispatcher =
new AsyncEventDispatcher(this,
NS_LITERAL_STRING("input"),
CanBubble::eYes,
ChromeOnlyDispatch::eNo);
asyncDispatcher->RunDOMEventWhenSafe();
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch input event");
}
}
@ -3778,11 +3790,9 @@ HTMLInputElement::SetValueOfRangeForUserEvent(Decimal aValue)
}
if (GetValueAsDecimal() != oldValue) {
nsContentUtils::DispatchTrustedEvent(OwnerDoc(),
static_cast<Element*>(this),
NS_LITERAL_STRING("input"),
CanBubble::eYes,
Cancelable::eNo);
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch input event");
}
}
@ -3877,11 +3887,9 @@ HTMLInputElement::StepNumberControlForUserEvent(int32_t aDirection)
SetValueInternal(newVal, nsTextEditorState::eSetValue_BySetUserInput |
nsTextEditorState::eSetValue_Notify);
nsContentUtils::DispatchTrustedEvent(OwnerDoc(),
static_cast<Element*>(this),
NS_LITERAL_STRING("input"),
CanBubble::eYes,
Cancelable::eNo);
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch input event");
}
static bool
@ -4091,9 +4099,9 @@ HTMLInputElement::PostHandleEvent(EventChainPostVisitor& aVisitor)
}
} else {
// Fire input event and then change event.
nsContentUtils::DispatchTrustedEvent<InternalEditorInputEvent>
(OwnerDoc(), static_cast<Element*>(this),
eEditorInput, CanBubble::eYes, Cancelable::eNo);
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch input event");
nsContentUtils::DispatchTrustedEvent<WidgetEvent>
(OwnerDoc(), static_cast<Element*>(this),

View File

@ -163,9 +163,11 @@ public:
virtual void AsyncEventRunning(AsyncEventDispatcher* aEvent) override;
// Overriden nsIFormControl methods
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD Reset() override;
NS_IMETHOD SubmitNamesValues(HTMLFormSubmission* aFormSubmission) override;
NS_IMETHOD SaveState() override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
virtual bool RestoreState(PresState* aState) override;
virtual bool AllowDrop() override;
virtual bool IsDisabledForEvents(WidgetEvent* aEvent) override;
@ -186,13 +188,20 @@ public:
virtual nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
virtual nsresult PreHandleEvent(EventChainVisitor& aVisitor) override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
virtual nsresult PostHandleEvent(
EventChainPostVisitor& aVisitor) override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
void PostHandleEventForRangeThumb(EventChainPostVisitor& aVisitor);
MOZ_CAN_RUN_SCRIPT
void StartRangeThumbDrag(WidgetGUIEvent* aEvent);
MOZ_CAN_RUN_SCRIPT
void FinishRangeThumbDrag(WidgetGUIEvent* aEvent = nullptr);
MOZ_CAN_RUN_SCRIPT
void CancelRangeThumbDrag(bool aIsForUserEvent = true);
MOZ_CAN_RUN_SCRIPT
void SetValueOfRangeForUserEvent(Decimal aValue);
virtual nsresult BindToTree(nsIDocument* aDocument, nsIContent* aParent,
@ -200,6 +209,7 @@ public:
virtual void UnbindFromTree(bool aDeep = true,
bool aNullParent = true) override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
virtual void DoneCreatingElement() override;
virtual EventStates IntrinsicState() const override;
@ -223,10 +233,13 @@ public:
NS_IMETHOD_(bool) ValueChanged() const override;
NS_IMETHOD_(void) GetTextEditorValue(nsAString& aValue, bool aIgnoreWrap) const override;
NS_IMETHOD_(mozilla::TextEditor*) GetTextEditor() override;
NS_IMETHOD_(mozilla::TextEditor*) GetTextEditorWithoutCreation() override;
NS_IMETHOD_(nsISelectionController*) GetSelectionController() override;
NS_IMETHOD_(nsFrameSelection*) GetConstFrameSelection() override;
NS_IMETHOD BindToFrame(nsTextControlFrame* aFrame) override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD_(void) UnbindFromFrame(nsTextControlFrame* aFrame) override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD CreateEditor() override;
NS_IMETHOD_(void) UpdateOverlayTextVisibility(bool aNotify) override;
NS_IMETHOD_(void) SetPreviewValue(const nsAString& aValue) override;
@ -238,6 +251,7 @@ public:
NS_IMETHOD_(void) InitializeKeyboardEventListeners() override;
NS_IMETHOD_(void) OnValueChanged(bool aNotify, bool aWasInteractiveUserChange) override;
virtual void GetValueFromSetRangeText(nsAString& aValue) override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
virtual nsresult SetValueFromSetRangeText(const nsAString& aValue) override;
NS_IMETHOD_(bool) HasCachedSelection() override;
@ -277,6 +291,7 @@ public:
*/
HTMLInputElement* GetSelectedRadioButton() const;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLInputElement,
@ -717,6 +732,7 @@ public:
SetHTMLAttr(nsGkAtoms::value, aValue, aRv);
}
MOZ_CAN_RUN_SCRIPT_BOUNDARY
void SetValue(const nsAString& aValue, CallerType aCallerType,
ErrorResult& aRv);
void GetValue(nsAString& aValue, CallerType aCallerType);
@ -891,12 +907,14 @@ public:
};
void StopNumberControlSpinnerSpin(SpinnerStopState aState =
eAllowDispatchingEvents);
MOZ_CAN_RUN_SCRIPT
void StepNumberControlForUserEvent(int32_t aDirection);
/**
* The callback function used by the nsRepeatService that we use to spin the
* spinner for <input type=number>.
*/
MOZ_CAN_RUN_SCRIPT_BOUNDARY
static void HandleNumberControlSpin(void* aData);
bool NumberSpinnerUpButtonIsDepressed() const
@ -916,6 +934,12 @@ public:
*/
nsIEditor* GetEditor();
bool IsInputEventTarget() const
{
return IsSingleLineTextControl(false);
}
MOZ_CAN_RUN_SCRIPT_BOUNDARY
void SetUserInput(const nsAString& aInput,
nsIPrincipal& aSubjectPrincipal);
@ -1009,10 +1033,12 @@ protected:
If previous value is unknown, aOldValue can be nullptr.
* @param aFlags See nsTextEditorState::SetValueFlags.
*/
MOZ_CAN_RUN_SCRIPT
nsresult SetValueInternal(const nsAString& aValue,
const nsAString* aOldValue,
uint32_t aFlags);
MOZ_CAN_RUN_SCRIPT
nsresult SetValueInternal(const nsAString& aValue,
uint32_t aFlags)
{
@ -1054,6 +1080,7 @@ protected:
/**
* Called when an attribute has just been changed
*/
MOZ_CAN_RUN_SCRIPT_BOUNDARY
virtual nsresult AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
const nsAttrValue* aValue,
const nsAttrValue* aOldValue,
@ -1180,6 +1207,7 @@ protected:
/**
* Manages the internal data storage across type changes.
*/
MOZ_CAN_RUN_SCRIPT
void HandleTypeChange(uint8_t aNewType, bool aNotify);
/**
@ -1198,6 +1226,7 @@ protected:
* @note You should not call this method if GetValueMode() doesn't return
* VALUE_MODE_VALUE.
*/
MOZ_CAN_RUN_SCRIPT
nsresult SetDefaultValueAsValue();
void SetDirectionFromValue(bool aNotify);

View File

@ -224,6 +224,12 @@ HTMLTextAreaElement::GetTextEditor()
return mState.GetTextEditor();
}
NS_IMETHODIMP_(TextEditor*)
HTMLTextAreaElement::GetTextEditorWithoutCreation()
{
return mState.GetTextEditorWithoutCreation();
}
NS_IMETHODIMP_(nsISelectionController*)
HTMLTextAreaElement::GetSelectionController()
{

View File

@ -61,6 +61,7 @@ public:
}
// nsIFormControl
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD Reset() override;
NS_IMETHOD SubmitNamesValues(HTMLFormSubmission* aFormSubmission) override;
NS_IMETHOD SaveState() override;
@ -83,10 +84,13 @@ public:
NS_IMETHOD_(bool) ValueChanged() const override;
NS_IMETHOD_(void) GetTextEditorValue(nsAString& aValue, bool aIgnoreWrap) const override;
NS_IMETHOD_(mozilla::TextEditor*) GetTextEditor() override;
NS_IMETHOD_(mozilla::TextEditor*) GetTextEditorWithoutCreation() override;
NS_IMETHOD_(nsISelectionController*) GetSelectionController() override;
NS_IMETHOD_(nsFrameSelection*) GetConstFrameSelection() override;
NS_IMETHOD BindToFrame(nsTextControlFrame* aFrame) override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD_(void) UnbindFromFrame(nsTextControlFrame* aFrame) override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD CreateEditor() override;
NS_IMETHOD_(void) UpdateOverlayTextVisibility(bool aNotify) override;
NS_IMETHOD_(bool) GetPlaceholderVisibility() override;
@ -98,6 +102,7 @@ public:
NS_IMETHOD_(void) InitializeKeyboardEventListeners() override;
NS_IMETHOD_(void) OnValueChanged(bool aNotify, bool aWasInteractiveUserChange) override;
virtual void GetValueFromSetRangeText(nsAString& aValue) override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
virtual nsresult SetValueFromSetRangeText(const nsAString& aValue) override;
NS_IMETHOD_(bool) HasCachedSelection() override;
@ -127,6 +132,7 @@ public:
virtual void DoneAddingChildren(bool aHaveNotified) override;
virtual bool IsDoneAddingChildren() override;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
nsresult CopyInnerTo(Element* aDest);
@ -277,6 +283,7 @@ public:
void GetDefaultValue(nsAString& aDefaultValue, ErrorResult& aError);
void SetDefaultValue(const nsAString& aDefaultValue, ErrorResult& aError);
void GetValue(nsAString& aValue);
MOZ_CAN_RUN_SCRIPT_BOUNDARY
void SetValue(const nsAString& aValue, ErrorResult& aError);
uint32_t GetTextLength();
@ -302,6 +309,12 @@ public:
return mState.GetTextEditor();
}
bool IsInputEventTarget() const
{
return true;
}
MOZ_CAN_RUN_SCRIPT_BOUNDARY
void SetUserInput(const nsAString& aValue,
nsIPrincipal& aSubjectPrincipal);
@ -358,6 +371,7 @@ protected:
* @param aValue String to set.
* @param aFlags See nsTextEditorState::SetValueFlags.
*/
MOZ_CAN_RUN_SCRIPT
nsresult SetValueInternal(const nsAString& aValue, uint32_t aFlags);
/**

View File

@ -126,7 +126,8 @@ InputType::GetNonFileValueInternal(nsAString& aValue) const
nsresult
InputType::SetValueInternal(const nsAString& aValue, uint32_t aFlags)
{
return mInputElement->SetValueInternal(aValue, aFlags);
RefPtr<mozilla::dom::HTMLInputElement> inputElement(mInputElement);
return inputElement->SetValueInternal(aValue, aFlags);
}
mozilla::Decimal

View File

@ -130,6 +130,7 @@ protected:
* @param aValue String to set.
* @param aFlags See nsTextEditorState::SetValueFlags.
*/
MOZ_CAN_RUN_SCRIPT
nsresult SetValueInternal(const nsAString& aValue, uint32_t aFlags);
/**

View File

@ -67,6 +67,7 @@ public:
return new (aMemory) RangeInputType(aInputElement);
}
MOZ_CAN_RUN_SCRIPT
nsresult MinMaxStepAttrChanged() override;
private:

View File

@ -102,8 +102,12 @@ public:
* Get the editor object associated with the text editor.
* The return value is null if the control does not support an editor
* (for example, if it is a checkbox.)
* Note that GetTextEditor() creates editor if it hasn't been created yet.
* If you need editor only when the editor is there, you should use
* GetTextEditorWithoutCreation().
*/
NS_IMETHOD_(mozilla::TextEditor*) GetTextEditor() = 0;
NS_IMETHOD_(mozilla::TextEditor*) GetTextEditorWithoutCreation() = 0;
/**
* Get the selection controller object associated with the text editor.

View File

@ -61,10 +61,10 @@ SetEditorFlagsIfNecessary(EditorBase& aEditorBase, uint32_t aFlags)
return aEditorBase.SetFlags(aFlags);
}
class MOZ_STACK_CLASS ValueSetter
class MOZ_STACK_CLASS AutoInputEventSuppresser final
{
public:
explicit ValueSetter(TextEditor* aTextEditor)
explicit AutoInputEventSuppresser(TextEditor* aTextEditor)
: mTextEditor(aTextEditor)
// To protect against a reentrant call to SetValue, we check whether
// another SetValue is already happening for this editor. If it is,
@ -73,7 +73,7 @@ public:
{
MOZ_ASSERT(aTextEditor);
}
~ValueSetter()
~AutoInputEventSuppresser()
{
mTextEditor->SuppressDispatchingInputEvent(mOuterTransaction);
}
@ -1223,6 +1223,12 @@ nsTextEditorState::GetTextEditor()
return mTextEditor;
}
TextEditor*
nsTextEditorState::GetTextEditorWithoutCreation()
{
return mTextEditor;
}
nsISelectionController*
nsTextEditorState::GetSelectionController() const
{
@ -2394,6 +2400,10 @@ nsTextEditorState::SetValue(const nsAString& aValue, const nsAString* aOldValue,
return false;
}
// mTextCtrlElement may be cleared when we dispatch an event so that
// we should keep grabbing it with local variable.
nsCOMPtr<nsITextControlElement> textControlElement(mTextCtrlElement);
if (mTextEditor && mBoundFrame) {
// The InsertText call below might flush pending notifications, which
// could lead into a scheduled PrepareEditor to be called. That will
@ -2425,7 +2435,7 @@ nsTextEditorState::SetValue(const nsAString& aValue, const nsAString* aOldValue,
// this is necessary to avoid infinite recursion
if (!currentValue.Equals(newValue)) {
RefPtr<TextEditor> textEditor = mTextEditor;
ValueSetter valueSetter(textEditor);
AutoInputEventSuppresser suppressInputEventDispatching(textEditor);
nsCOMPtr<nsIDocument> document = textEditor->GetDocument();
if (NS_WARN_IF(!document)) {
@ -2448,8 +2458,6 @@ nsTextEditorState::SetValue(const nsAString& aValue, const nsAString* aOldValue,
return true;
}
valueSetter.Init();
// get the flags, remove readonly, disabled and max-length,
// set the value, restore flags
{
@ -2464,10 +2472,16 @@ nsTextEditorState::SetValue(const nsAString& aValue, const nsAString* aOldValue,
// autocomplete, we need to replace the text as "insert string"
// because undo should cancel only this operation (i.e., previous
// transactions typed by user shouldn't be merged with this).
// In this case, we need to dispatch "input" event because
// web apps may need to know the user's operation.
DebugOnly<nsresult> rv = textEditor->ReplaceTextAsAction(newValue);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to set the new value");
} else if (aFlags & eSetValue_ForXUL) {
// When setting value of XUL <textbox>, we shouldn't dispatch
// "input" event.
suppressInputEventDispatching.Init();
// On XUL <textbox> element, we need to preserve existing undo
// transactions.
// XXX Do we really need to do such complicated optimization?
@ -2505,6 +2519,10 @@ nsTextEditorState::SetValue(const nsAString& aValue, const nsAString* aOldValue,
"Failed to insert the new value");
}
} else {
// When setting value of <input>, we shouldn't dispatch "input"
// event.
suppressInputEventDispatching.Init();
// On <input> or <textarea>, we shouldn't preserve existing undo
// transactions because other browsers do not preserve them too
// and not preserving transactions makes setting value faster.
@ -2584,6 +2602,18 @@ nsTextEditorState::SetValue(const nsAString& aValue, const nsAString* aOldValue,
if (mBoundFrame) {
mBoundFrame->UpdateValueDisplay(true);
}
// If this is called as part of user input, we need to dispatch "input"
// event since web apps may want to know the user operation.
if (aFlags & eSetValue_BySetUserInput) {
nsCOMPtr<Element> element = do_QueryInterface(textControlElement);
MOZ_ASSERT(element);
RefPtr<TextEditor> textEditor;
DebugOnly<nsresult> rvIgnored =
nsContentUtils::DispatchInputEvent(element, textEditor);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch input event");
}
} else {
// Even if our value is not actually changing, apparently we need to mark
// our SelectionProperties dirty to make accessibility tests happy.
@ -2600,8 +2630,10 @@ nsTextEditorState::SetValue(const nsAString& aValue, const nsAString* aOldValue,
ValueWasChanged(!!mBoundFrame);
}
mTextCtrlElement->OnValueChanged(/* aNotify = */ !!mBoundFrame,
/* aWasInteractiveUserChange = */ false);
// XXX Should we stop notifying "value changed" if mTextCtrlElement has
// been cleared?
textControlElement->OnValueChanged(/* aNotify = */ !!mBoundFrame,
/* aWasInteractiveUserChange = */ false);
return true;
}

View File

@ -152,10 +152,13 @@ public:
}
mozilla::TextEditor* GetTextEditor();
mozilla::TextEditor* GetTextEditorWithoutCreation();
nsISelectionController* GetSelectionController() const;
nsFrameSelection* GetConstFrameSelection();
nsresult BindToFrame(nsTextControlFrame* aFrame);
MOZ_CAN_RUN_SCRIPT_BOUNDARY
void UnbindFromFrame(nsTextControlFrame* aFrame);
MOZ_CAN_RUN_SCRIPT_BOUNDARY
nsresult PrepareEditor(const nsAString *aValue = nullptr);
void InitializeKeyboardEventListeners();
@ -181,9 +184,11 @@ public:
// undo history.
eSetValue_ForXUL = 1 << 4,
};
MOZ_CAN_RUN_SCRIPT
MOZ_MUST_USE bool SetValue(const nsAString& aValue,
const nsAString* aOldValue,
uint32_t aFlags);
MOZ_CAN_RUN_SCRIPT
MOZ_MUST_USE bool SetValue(const nsAString& aValue,
uint32_t aFlags)
{
@ -303,6 +308,7 @@ public:
// Sync up our selection properties with our editor prior to being destroyed.
// This will invoke UnbindFromFrame() to ensure that we grab whatever
// selection state may be at the moment.
MOZ_CAN_RUN_SCRIPT
void SyncUpSelectionPropertiesBeforeDestruction();
// Get the selection range start and end points in our text.

View File

@ -82,6 +82,7 @@ skip-if = os == "android" && debug # bug 1397615
[test_meter_element.html]
[test_meter_pseudo-classes.html]
[test_min_attribute.html]
[test_MozEditableElement_setUserInput.html]
[test_mozistextfield.html]
[test_novalidate_attribute.html]
[test_option_disabled.html]

View File

@ -0,0 +1,239 @@
<!DOCTYPE>
<html>
<head>
<title>Test for MozEditableElement.setUserInput()</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css">
</head>
<body>
<div id="display">
</div>
<div id="content"></div>
<pre id="test">
</pre>
<script class="testbody" type="application/javascript">
SimpleTest.waitForExplicitFinish();
SimpleTest.waitForFocus(() => {
let content = document.getElementById("content");
/**
* Test structure:
* element: the tag name to create.
* type: the type attribute value for the element. If unnecessary omit it.
* input: the values calling setUserInput() with.
* before: used when calling setUserInput() before the element gets focus.
* after: used when calling setUserInput() after the element gets focus.
* result: the results of calling setUserInput().
* before: the element's expected value of calling setUserInput() before the element gets focus.
* after: the element's expected value of calling setUserInput() after the element gets focus.
* fireInputEvent: true if "input" event should be fired. Otherwise, false.
*/
for (let test of [{element: "input", type: "hidden",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: false}},
{element: "input", type: "text",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
{element: "input", type: "search",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
{element: "input", type: "tel",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
{element: "input", type: "url",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
{element: "input", type: "email",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
{element: "input", type: "password",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
// "date" does not support setUserInput, but dispatches "input" event...
{element: "input", type: "date",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
// "month" does not support setUserInput, but dispatches "input" event...
{element: "input", type: "month",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
// "week" does not support setUserInput, but dispatches "input" event...
{element: "input", type: "week",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
// "time" does not support setUserInput, but dispatches "input" event...
{element: "input", type: "time",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
// "datetime-local" does not support setUserInput, but dispatches "input" event...
{element: "input", type: "datetime-local",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
{element: "input", type: "number",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
{element: "input", type: "range",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
// "color" does not support setUserInput, but dispatches "input" event...
{element: "input", type: "color",
input: {before: "#5C5C5C", after: "#FFFFFF"},
result: {before: "#5c5c5c", after:"#ffffff", fireInputEvent: true}},
{element: "input", type: "checkbox",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
{element: "input", type: "radio",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}},
// "file" is not supported by setUserInput? But there is a path...
{element: "input", type: "file",
input: {before: "3", after: "6"},
result: {before: "", after:"", fireInputEvent: true}},
{element: "input", type: "submit",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: false}},
{element: "input", type: "image",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: false}},
{element: "input", type: "reset",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: false}},
{element: "input", type: "button",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: false}},
{element: "textarea",
input: {before: "3", after: "6"},
result: {before: "3", after:"6", fireInputEvent: true}}]) {
let tag =
test.type !== undefined ? `<${test.element} type="${test.type}">` :
`<${test.element}>`;
content.innerHTML =
test.element !== "input" ? tag : `${tag}</${test.element}>`;
content.scrollTop; // Flush pending layout.
let target = content.firstChild;
let inputEvents = [];
function onInput(aEvent) {
inputEvents.push(aEvent);
}
target.addEventListener("input", onInput);
// Before setting focus, editor of the element may have not been created yet.
let previousValue = target.value;
SpecialPowers.wrap(target).setUserInput(test.input.before);
if (target.value == previousValue && test.result.before != previousValue) {
todo_is(target.value, test.result.before, `setUserInput("${test.input.before}") before ${tag} gets focus should set its value to "${test.result.before}"`);
} else {
is(target.value, test.result.before, `setUserInput("${test.input.before}") before ${tag} gets focus should set its value to "${test.result.before}"`);
}
if (target.value == previousValue) {
if (test.type === "date" || test.type === "time") {
todo_is(inputEvents.length, 0,
`No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
} else {
is(inputEvents.length, 0,
`No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
}
} else if (!test.result.fireInputEvent) {
// HTML spec defines that "input" elements whose type are "hidden",
// "submit", "image", "reset" and "button" shouldn't fire input event
// when its value is changed.
// XXX Perhaps, we shouldn't support setUserInput() with such types.
if (test.type === "hidden" ||
test.type === "submit" ||
test.type === "image" ||
test.type === "reset" ||
test.type === "button") {
todo_is(inputEvents.length, 0,
`No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
} else {
is(inputEvents.length, 0,
`No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
}
} else {
is(inputEvents.length, 1,
`Only one "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
}
if (inputEvents.length > 0) {
if (SpecialPowers.wrap(target).isInputEventTarget) {
if (test.type === "number" || test.type === "time") {
todo(inputEvents[0] instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
} else {
ok(inputEvents[0] instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
}
} else {
ok(inputEvents[0] instanceof Event && !(inputEvents[0] instanceof UIEvent),
`"input" event should be dispatched with Event interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
}
is(inputEvents[0].cancelable, false,
`"input" event should be never cancelable (${tag}, before getting focus)`);
is(inputEvents[0].bubbles, true,
`"input" event should always bubble (${tag}, before getting focus)`);
}
inputEvents = [];
target.focus();
previousValue = target.value;
SpecialPowers.wrap(target).setUserInput(test.input.after);
if (target.value == previousValue && test.result.after != previousValue) {
todo_is(target.value, test.result.after, `setUserInput("${test.input.after}") after ${tag} gets focus should set its value to "${test.result.after}"`);
} else {
is(target.value, test.result.after, `setUserInput("${test.input.after}") after ${tag} gets focus should set its value to "${test.result.after}"`);
}
if (target.value == previousValue) {
if (test.type === "date" || test.type === "time") {
todo_is(inputEvents.length, 0,
`No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
} else {
is(inputEvents.length, 0,
`No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
}
} else if (!test.result.fireInputEvent) {
// HTML spec defines that "input" elements whose type are "hidden",
// "submit", "image", "reset" and "button" shouldn't fire input event
// when its value is changed.
// XXX Perhaps, we shouldn't support setUserInput() with such types.
if (test.type === "hidden" ||
test.type === "submit" ||
test.type === "image" ||
test.type === "reset" ||
test.type === "button") {
todo_is(inputEvents.length, 0,
`No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
} else {
is(inputEvents.length, 0,
`No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
}
} else {
is(inputEvents.length, 1,
`Only one "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
}
if (inputEvents.length > 0) {
if (SpecialPowers.wrap(target).isInputEventTarget) {
if (test.type === "number" || test.type === "time") {
todo(inputEvents[0] instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
} else {
ok(inputEvents[0] instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
}
} else {
ok(inputEvents[0] instanceof Event && !(inputEvents[0] instanceof UIEvent),
`"input" event should be dispatched with Event interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
}
is(inputEvents[0].cancelable, false,
`"input" event should be never cancelable (${tag}, after getting focus)`);
is(inputEvents[0].bubbles, true,
`"input" event should always bubble (${tag}, after getting focus)`);
}
target.removeEventListener("input", onInput);
}
SimpleTest.finish();
});
</script>
</body>
</html>

View File

@ -13,24 +13,24 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=851780
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=851780">Mozilla Bug 851780</a>
<p id="display"></p>
<div id="content">
<input type="file" id="fileInput"></input>
<textarea id="textarea" oninput="++textareaInput;"></textarea>
<input type="text" id="input_text" oninput="++textInput[0];"></input>
<input type="email" id="input_email" oninput="++textInput[1];"></input>
<input type="search" id="input_search" oninput="++textInput[2];"></input>
<input type="tel" id="input_tel" oninput="++textInput[3];"></input>
<input type="url" id="input_url" oninput="++textInput[4];"></input>
<input type="password" id="input_password" oninput="++textInput[5];"></input>
<input type="file" id="fileInput">
<textarea id="textarea"></textarea>
<input type="text" id="input_text">
<input type="email" id="input_email">
<input type="search" id="input_search">
<input type="tel" id="input_tel">
<input type="url" id="input_url">
<input type="password" id="input_password">
<!-- "Non-text" inputs-->
<input type="button" id="input_button" oninput="++NonTextInput[0];"></input>
<input type="submit" id="input_submit" oninput="++NonTextInput[1];"></input>
<input type="image" id="input_image" oninput="++NonTextInput[2];"></input>
<input type="reset" id="input_reset" oninput="++NonTextInput[3];"></input>
<input type="radio" id="input_radio" oninput="++NonTextInput[4];"></input>
<input type="checkbox" id="input_checkbox" oninput="++NonTextInput[5];"></input>
<input type="range" id="input_range" oninput="++rangeInput;"></input>
<input type="number" id="input_number" oninput="++numberInput;"></input>
<input type="button" id="input_button">
<input type="submit" id="input_submit">
<input type="image" id="input_image">
<input type="reset" id="input_reset">
<input type="radio" id="input_radio">
<input type="checkbox" id="input_checkbox">
<input type="range" id="input_range">
<input type="number" id="input_number">
</div>
<pre id="test">
@ -40,18 +40,68 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=851780
const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
var textareaInput = 0;
function checkIfInputIsInputEvent(aEvent, aToDo, aDescription) {
if (aToDo) {
// Probably, key operation should fire "input" event with InputEvent interface.
// See https://github.com/w3c/input-events/issues/88
todo(aEvent instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface ${aDescription}`);
} else {
ok(aEvent instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface ${aDescription}`);
}
is(aEvent.cancelable, false,
`"input" event should be never cancelable ${aDescription}`);
is(aEvent.bubbles, true,
`"input" event should always bubble ${aDescription}`);
}
// Those are types were the input event apply.
function checkIfInputIsEvent(aEvent, aDescription) {
ok(event instanceof Event && !(event instanceof UIEvent),
`"input" event should be dispatched with InputEvent interface ${aDescription}`);
is(aEvent.cancelable, false,
`"input" event should be never cancelable ${aDescription}`);
is(aEvent.bubbles, true,
`"input" event should always bubble ${aDescription}`);
}
var textareaInput = 0;
document.getElementById("textarea").oninput = (aEvent) => {
++textareaInput;
checkIfInputIsInputEvent(aEvent, false, "on textarea element");
};
// These are the type were the input event apply.
var textTypes = ["text", "email", "search", "tel", "url", "password"];
var textInput = [0, 0, 0, 0, 0, 0];
for (let id of ["input_text", "input_email", "input_search", "input_tel", "input_url", "input_password"]) {
document.getElementById(id).oninput = (aEvent) => {
++textInput[textTypes.indexOf(aEvent.target.type)];
checkIfInputIsInputEvent(aEvent, false, `on input element whose type is ${aEvent.target.type}`);
};
}
// Those are events were the input event does not apply.
// These are the type were the input event does not apply.
var NonTextTypes = ["button", "submit", "image", "reset", "radio", "checkbox"];
var NonTextInput = [0, 0, 0, 0, 0, 0];
for (let id of ["input_button", "input_submit", "input_image", "input_reset", "input_radio", "input_checkbox"]) {
document.getElementById(id).oninput = (aEvent) => {
++NonTextInput[NonTextTypes.indexOf(aEvent.target.type)];
checkIfInputIsEvent(aEvent, `on input element whose type is ${aEvent.target.type}`);
};
}
var rangeInput = 0;
document.getElementById("input_range").oninput = (aEvent) => {
++rangeInput;
checkIfInputIsEvent(aEvent, "on input element whose type is range");
};
var numberInput = 0;
document.getElementById("input_number").oninput = (aEvent) => {
++numberInput;
checkIfInputIsInputEvent(aEvent, true, "on input element whose type is number");
};
SimpleTest.waitForExplicitFinish();
var MockFilePicker = SpecialPowers.MockFilePicker;
@ -66,6 +116,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=851780
input.addEventListener("input", function (aEvent) {
ok(true, "input event should have been dispatched on file input.");
checkIfInputIsEvent(aEvent, "on file input");
});
input.click();

View File

@ -48,6 +48,14 @@ const SPIN_UP_Y = 3;
const SPIN_DOWN_X = inputRect.width - 3;
const SPIN_DOWN_Y = inputRect.height - 3;
function checkInputEvent(aEvent, aDescription) {
// Probably, key operation should fire "input" event with InputEvent interface.
// See https://github.com/w3c/input-events/issues/88
todo(aEvent instanceof InputEvent, `"input" event should be dispatched with InputEvent interface on input element whose type is number ${aDescription}`);
is(aEvent.cancelable, false, `"input" event should be never cancelable on input element whose type is number ${aDescription}`);
is(aEvent.bubbles, true, `"input" event should always bubble on input element whose type is number ${aDescription}`);
}
function test() {
input.value = 0;
@ -134,6 +142,7 @@ var spinTests = [
input.value = 0;
input.addEventListener("input", function(evt) {
++inputEventCount;
checkInputEvent(event, "#1");
if (inputEventCount == 3) {
is(input.value, "3", "Testing spin-up button");
synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousemove" });
@ -154,6 +163,7 @@ var spinTests = [
input.value = 0;
input.addEventListener("input", function(evt) {
++inputEventCount;
checkInputEvent(event, "#2");
if (inputEventCount == 3) {
is(input.value, "-3", "Testing spin-down button");
synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousemove" });
@ -174,6 +184,7 @@ var spinTests = [
input.value = 0;
input.addEventListener("input", function(evt) {
++inputEventCount;
checkInputEvent(event, "#3");
if (inputEventCount == 3) {
synthesizeMouse(input, -1, -1, { type: "mousemove" });
var eventHandler = arguments.callee;
@ -195,6 +206,7 @@ var spinTests = [
input.value = 0;
input.addEventListener("input", function(evt) {
++inputEventCount;
checkInputEvent(event, "#4");
if (inputEventCount == 3) {
input.type = "text"
var eventHandler = arguments.callee;

View File

@ -192,6 +192,12 @@ interface MozEditableElement {
[Pure, ChromeOnly]
readonly attribute nsIEditor? editor;
// This is set to true if "input" event should be fired with InputEvent on
// the element. Otherwise, i.e., if "input" event should be fired with
// Event, set to false.
[Func="IsChromeOrXBLOrUAWidget"]
readonly attribute boolean isInputEventTarget;
// This is similar to set .value on nsIDOMInput/TextAreaElements, but handling
// of the value change is closer to the normal user input, so 'change' event
// for example will be dispatched when focusing out the element.

View File

@ -2142,56 +2142,6 @@ EditorBase::NotifySelectionChanged(nsIDocument* aDocument,
return NS_OK;
}
class EditorInputEventDispatcher final : public Runnable
{
public:
EditorInputEventDispatcher(EditorBase* aEditorBase,
nsIContent* aTarget,
bool aIsComposing)
: Runnable("EditorInputEventDispatcher")
, mEditorBase(aEditorBase)
, mTarget(aTarget)
, mIsComposing(aIsComposing)
{
}
NS_IMETHOD Run() override
{
// Note that we don't need to check mDispatchInputEvent here. We need
// to check it only when the editor requests to dispatch the input event.
if (!mTarget->IsInComposedDoc()) {
return NS_OK;
}
nsCOMPtr<nsIPresShell> ps = mEditorBase->GetPresShell();
if (!ps) {
return NS_OK;
}
nsCOMPtr<nsIWidget> widget = mEditorBase->GetWidget();
if (!widget) {
return NS_OK;
}
// Even if the change is caused by untrusted event, we need to dispatch
// trusted input event since it's a fact.
InternalEditorInputEvent inputEvent(true, eEditorInput, widget);
inputEvent.mTime = static_cast<uint64_t>(PR_Now() / 1000);
inputEvent.mIsComposing = mIsComposing;
nsEventStatus status = nsEventStatus_eIgnore;
nsresult rv =
ps->HandleEventWithTarget(&inputEvent, nullptr, mTarget, &status);
NS_ENSURE_SUCCESS(rv, NS_OK); // print the warning if error
return NS_OK;
}
private:
RefPtr<EditorBase> mEditorBase;
nsCOMPtr<nsIContent> mTarget;
bool mIsComposing;
};
void
EditorBase::NotifyEditorObservers(NotificationForEditorObservers aNotification)
{
@ -2252,19 +2202,15 @@ EditorBase::NotifyEditorObservers(NotificationForEditorObservers aNotification)
void
EditorBase::FireInputEvent()
{
// We don't need to dispatch multiple input events if there is a pending
// input event. However, it may have different event target. If we resolved
// this issue, we need to manage the pending events in an array. But it's
// overwork. We don't need to do it for the very rare case.
nsCOMPtr<nsIContent> target = GetInputEventTargetContent();
NS_ENSURE_TRUE_VOID(target);
// NOTE: Don't refer IsIMEComposing() because it returns false even before
// compositionend. However, DOM Level 3 Events defines it should be
// true after compositionstart and before compositionend.
nsContentUtils::AddScriptRunner(
new EditorInputEventDispatcher(this, target, !!GetComposition()));
RefPtr<Element> targetElement = GetInputEventTargetElement();
if (NS_WARN_IF(!targetElement)) {
return;
}
RefPtr<TextEditor> textEditor = AsTextEditor();
DebugOnly<nsresult> rvIgnored =
nsContentUtils::DispatchInputEvent(targetElement, textEditor);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch input event");
}
NS_IMETHODIMP

View File

@ -356,6 +356,7 @@ public:
/**
* ToggleTextDirection() toggles text-direction of the root element.
*/
MOZ_CAN_RUN_SCRIPT_BOUNDARY
nsresult ToggleTextDirection();
/**
@ -367,6 +368,7 @@ public:
eLTR,
eRTL,
};
MOZ_CAN_RUN_SCRIPT
void SwitchTextDirectionTo(TextDirection aTextDirection);
/**
@ -1732,7 +1734,9 @@ protected: // Called by helper classes.
* can later merge, if needed. Merging is unavailable between transaction
* manager batches.
*/
MOZ_CAN_RUN_SCRIPT_BOUNDARY
void BeginPlaceholderTransaction(nsAtom* aTransactionName);
MOZ_CAN_RUN_SCRIPT_BOUNDARY
void EndPlaceholderTransaction();
void BeginUpdateViewBatch();
@ -1768,6 +1772,8 @@ protected: // Shouldn't be used by friend classes
virtual nsresult SelectAllInternal();
nsresult DetermineCurrentDirection();
MOZ_CAN_RUN_SCRIPT
void FireInputEvent();
/**
@ -1888,7 +1894,7 @@ protected: // Shouldn't be used by friend classes
/**
* Get the input event target. This might return null.
*/
virtual already_AddRefed<nsIContent> GetInputEventTargetContent() = 0;
virtual already_AddRefed<Element> GetInputEventTargetElement() = 0;
/**
* Return true if spellchecking should be enabled for this editor.
@ -1950,6 +1956,7 @@ protected: // Shouldn't be used by friend classes
eNotifyEditorObserversOfBefore,
eNotifyEditorObserversOfCancel
};
MOZ_CAN_RUN_SCRIPT
void NotifyEditorObservers(NotificationForEditorObservers aNotification);
private:

View File

@ -1031,7 +1031,7 @@ EditorEventListener::HandleChangeComposition(
return NS_OK;
}
TextEditor* textEditor = editorBase->AsTextEditor();
RefPtr<TextEditor> textEditor = editorBase->AsTextEditor();
return textEditor->OnCompositionChange(*aCompositionChangeEvent);
}
@ -1050,7 +1050,7 @@ EditorEventListener::HandleEndComposition(
MOZ_ASSERT(!aCompositionEndEvent->DefaultPrevented(),
"eCompositionEnd shouldn't be cancelable");
TextEditor* textEditor = editorBase->AsTextEditor();
RefPtr<TextEditor> textEditor = editorBase->AsTextEditor();
textEditor->OnCompositionEnd(*aCompositionEndEvent);
}

View File

@ -63,11 +63,14 @@ protected:
#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH
nsresult KeyDown(const WidgetKeyboardEvent* aKeyboardEvent);
MOZ_CAN_RUN_SCRIPT
nsresult KeyUp(const WidgetKeyboardEvent* aKeyboardEvent);
#endif
nsresult KeyPress(WidgetKeyboardEvent* aKeyboardEvent);
MOZ_CAN_RUN_SCRIPT
nsresult HandleChangeComposition(WidgetCompositionEvent* aCompositionEvent);
nsresult HandleStartComposition(WidgetCompositionEvent* aCompositionEvent);
MOZ_CAN_RUN_SCRIPT
void HandleEndComposition(WidgetCompositionEvent* aCompositionEvent);
MOZ_CAN_RUN_SCRIPT
virtual nsresult MouseDown(dom::MouseEvent* aMouseEvent);

View File

@ -5454,10 +5454,10 @@ HTMLEditor::GetPreferredIMEState(IMEState* aState)
return NS_OK;
}
already_AddRefed<nsIContent>
HTMLEditor::GetInputEventTargetContent()
already_AddRefed<Element>
HTMLEditor::GetInputEventTargetElement()
{
nsCOMPtr<nsIContent> target = GetActiveEditingHost();
RefPtr<Element> target = GetActiveEditingHost();
return target.forget();
}

View File

@ -1778,7 +1778,7 @@ protected: // Shouldn't be used by friend classes
*/
already_AddRefed<nsINode> GetFocusedNode();
virtual already_AddRefed<nsIContent> GetInputEventTargetContent() override;
virtual already_AddRefed<Element> GetInputEventTargetElement() override;
/**
* Return TRUE if aElement is a table-related elemet and caret was set.

View File

@ -229,6 +229,12 @@ HTMLEditor::InsertTableCellsWithTransaction(int32_t aNumberOfCellsToInsert,
MOZ_ASSERT_UNREACHABLE("Invalid InsertPosition");
}
AutoPlaceholderBatch treateAsOneTransaction(*this);
// Prevent auto insertion of BR in new cell until we're done
AutoTopLevelEditSubActionNotifier maybeTopLevelEditSubAction(
*this, EditSubAction::eInsertNode,
nsIEditor::eNext);
// We control selection resetting after the insert.
AutoSelectionSetterAfterTableEdit setCaret(*this, table,
cellDataAtSelection.mCurrent.mRow,

View File

@ -956,6 +956,7 @@ TextEditRules::WillSetText(bool* aCancel,
if (!IsPlaintextEditor() ||
TextEditorRef().IsIMEComposing() ||
TextEditorRef().IsUndoRedoEnabled() ||
TextEditorRef().GetEditAction() == EditAction::eReplaceText ||
aMaxLength != -1) {
// SetTextImpl only supports plain text editor without IME and
// when we don't need to make it undoable.

View File

@ -1460,10 +1460,10 @@ TextEditor::OnCompositionEnd(WidgetCompositionEvent& aCompositionEndEvent)
NotifyEditorObservers(eNotifyEditorObserversOfEnd);
}
already_AddRefed<nsIContent>
TextEditor::GetInputEventTargetContent()
already_AddRefed<Element>
TextEditor::GetInputEventTargetElement()
{
nsCOMPtr<nsIContent> target = do_QueryInterface(mEventTarget);
nsCOMPtr<Element> target = do_QueryInterface(mEventTarget);
return target.forget();
}

View File

@ -64,7 +64,9 @@ public:
// If there are some good name to create non-virtual Undo()/Redo() methods,
// we should create them and those methods should just run them.
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD Undo(uint32_t aCount) final;
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD Redo(uint32_t aCount) final;
NS_IMETHOD Cut() override;
@ -205,6 +207,7 @@ public:
* @param aCompositionChangeEvent eCompositionChange event which should
* be handled in this editor.
*/
MOZ_CAN_RUN_SCRIPT
nsresult
OnCompositionChange(WidgetCompositionEvent& aCompositionChangeEvent);
@ -213,6 +216,7 @@ public:
* event and it's followed by eCompositionEnd event and after
* OnCompositionChange() is called.
*/
MOZ_CAN_RUN_SCRIPT
void OnCompositionEnd(WidgetCompositionEvent& aCompositionEndEvent);
/**
@ -530,7 +534,7 @@ protected: // Shouldn't be used by friend classes
*/
bool EnsureComposition(WidgetCompositionEvent& aCompositionEvent);
virtual already_AddRefed<nsIContent> GetInputEventTargetContent() override;
virtual already_AddRefed<Element> GetInputEventTargetElement() override;
protected:
mutable nsCOMPtr<nsIDocumentEncoder> mCachedDocumentEncoder;

View File

@ -72,18 +72,39 @@ SimpleTest.waitForFocus(async function() {
// XXX Perhaps, we need to add border-top-width here if you add new test to have thick border.
const kPositionerY = -7;
let inputEventExpected = true;
function onInput(aEvent) {
if (!inputEventExpected) {
ok(false, "\"input\" event shouldn't be fired after stopping resizing");
return;
}
ok(aEvent instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface');
is(aEvent.cancelable, false,
'"input" event should be never cancelable');
is(aEvent.bubbles, true,
'"input" event should always bubble');
}
content.addEventListener("input", onInput);
// Click on the positioner.
synthesizeMouse(target, kPositionerX, kPositionerY, {type: "mousedown"});
// Drag it delta pixels.
synthesizeMouse(target, kPositionerX + aDeltaX, kPositionerY + aDeltaY, {type: "mousemove"});
// Release the mouse button
synthesizeMouse(target, kPositionerX + aDeltaX, kPositionerY + aDeltaY, {type: "mouseup"});
inputEventExpected = false;
// Move the mouse delta more pixels to the same direction to make sure that the
// positioning operation has stopped.
synthesizeMouse(target, kPositionerX + aDeltaX * 2, kPositionerY + aDeltaY * 2, {type: "mousemove"});
// Click outside of the image to hide the positioner.
synthesizeMouseAtCenter(outOfEditor, {});
content.removeEventListener("input", onInput);
// Get the new dimensions for the absolute positioned element.
let newRect = target.getBoundingClientRect();
isfuzzy(newRect.x, rect.x + aDeltaX, 1, description + "The left should be increased by " + aDeltaX + " pixels");

View File

@ -63,6 +63,8 @@ function runTests() {
var handler = function(aEvent) {
is(aEvent.target, eventTarget,
"input event is fired on unexpected element: " + aEvent.target.tagName);
ok(aEvent instanceof InputEvent,
"input event should be dispatched with InputEvent interface");
ok(!aEvent.cancelable, "input event must not be cancelable");
ok(aEvent.bubbles, "input event must be bubbles");
if (SpecialPowers.getBoolPref("dom.event.highrestimestamp.enabled")) {

View File

@ -37,6 +37,8 @@ function runTests() {
var handler = function(aEvent) {
is(aEvent.target, aElement,
"input event is fired on unexpected element: " + aEvent.target.tagName);
ok(aEvent instanceof InputEvent,
"input event should be dispatched with InputEvent interface");
ok(!aEvent.cancelable, "input event must not be cancelable");
ok(aEvent.bubbles, "input event must be bubbles");
if (SpecialPowers.getBoolPref("dom.event.highrestimestamp.enabled")) {

View File

@ -24,6 +24,13 @@ SimpleTest.waitForExplicitFinish();
var shouldClear = false;
window.addEventListener("dragstart", function(event) { if (shouldClear) event.dataTransfer.clearData(); }, true);
function checkInputEvent(aEvent, aExpectedTarget, aDescription) {
ok(aEvent instanceof InputEvent, `"input" event should be dispatched with InputEvent interface ${aDescription}`);
is(aEvent.cancelable, false, `"input" event should be never cancelable ${aDescription}`);
is(aEvent.bubbles, true, `"input" event should always bubble ${aDescription}`);
is(aEvent.target, aExpectedTarget, `"input" event should be fired on the <${aExpectedTarget.tagName.toLowerCase()}> element ${aDescription}`);
}
function doTest() {
const htmlContextData = { type: "text/_moz_htmlcontext",
data: "<html><body></body></html>" };
@ -39,56 +46,84 @@ function doTest() {
var input = document.getElementById("input");
var contenteditable = document.getElementById("contenteditable");
var inputEvents = [];
function onInput(event) {
inputEvents.push(event);
}
document.addEventListener("input", onInput);
var selection = window.getSelection();
// -------- Test dragging regular text
selection.selectAllChildren(text);
inputEvents = [];
var result = synthesizeDragStart(text, [[htmlContextData, htmlInfoData, htmlData,
{type: "text/plain", data: "Some Text"}]], window, 40, 10);
is(result, null, "Test dragging regular text");
is(inputEvents.length, 0,
'No "input" event should be fired when dragging from text in <span> element');
// -------- Test dragging text from an <input>
input.setSelectionRange(1, 4);
inputEvents = [];
result = synthesizeDragStart(input, [[{type: "text/plain", data: "rag"}]], window, 25, 8);
is(result, null, "Test dragging input");
is(inputEvents.length, 0,
'No "input" event should be fired when dragging from text in <input> element');
// -------- Test dragging text from a <textarea>
textarea.setSelectionRange(1, 7);
inputEvents = [];
result = synthesizeDragStart(textarea, [[{type: "text/plain", data: "ome Te"}]], window, 25, 6);
is(result, null, "Test dragging textarea");
textarea.blur();
is(inputEvents.length, 0,
'No "input" event should be fired when dragging from text in <textarea> element');
// -------- Test dragging text from a contenteditable
selection.selectAllChildren(contenteditable.childNodes[1]);
inputEvents = [];
result = synthesizeDragStart(contenteditable.childNodes[1],
[[htmlContextDataEditable, htmlInfoData,
{type: "text/html", data: '<b id="bold">editable</b>' },
{type: "text/plain", data: "editable"}]], window, 5, 6);
is(result, null, "Test dragging contenteditable");
is(inputEvents.length, 0,
'No "input" event should be fired when dragging from text in contenteditable element');
contenteditable.blur();
// -------- Test dragging regular text of text/html to <input>
selection.selectAllChildren(text);
input.value = "";
inputEvents = [];
synthesizeDrop(text, input, [], "copy");
is(input.value, "Some Text", "Drag text/html onto input");
is(inputEvents.length, 1,
'Only one "input" events should be fired when dropping text/html into empty <input> element');
checkInputEvent(inputEvents[0], input, "when dropping text/html into empty <input> element");
// -------- Test dragging regular text of text/html to disabled <input>
selection.selectAllChildren(text);
input.value = "";
input.disabled = true;
inputEvents = [];
synthesizeDrop(text, input, [], "copy");
is(input.value, "", "Drag text/html onto disabled input");
is(inputEvents.length, 0,
'No "input" event should be fired when dropping on disabled <input> element');
input.disabled = false;
// -------- Test dragging regular text of text/html to readonly <input>
selection.selectAllChildren(text);
input.readOnly = true;
inputEvents = [];
synthesizeDrop(text, input, [], "copy");
is(input.value, "", "Drag text/html onto readonly input");
is(inputEvents.length, 0,
'No "input" event should be fired when dropping on readonly <input> element');
input.readOnly = false;
// -------- Test dragging regular text of text/html to <input>. This sets
@ -98,16 +133,23 @@ function doTest() {
shouldClear = true;
selection.selectAllChildren(text);
input.value = "";
inputEvents = [];
synthesizeDrop(text, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"}]], "copy");
is(input.value, "", "Drag text/html onto input");
is(inputEvents.length, 0,
'No "input" event should be fired when dropping text/html into empty <input> element');
// -------- Test dragging regular text of text/plain and text/html to <input>
selection.selectAllChildren(text);
input.value = "";
inputEvents = [];
synthesizeDrop(text, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"},
{type: "text/plain", data: "Some Plain Text"}]], "copy");
is(input.value, "Some Plain Text", "Drag text/html and text/plain onto input");
is(inputEvents.length, 1,
'Only one "input" events should be fired when dropping text/plain into empty <input> element');
checkInputEvent(inputEvents[0], input, "when dropping text/plain into empty <input> element");
// -------- Test dragging regular text of text/plain to <textarea>
@ -116,44 +158,63 @@ function doTest() {
// synthesizeDrop(text, textarea, [[{type: "text/plain", data: "Somewhat Longer Text"}]], "copy");
// is(textarea.value, "Somewhat Longer Text", "Drag text/plain onto textarea");
// -------- Test dragging special text type of text/plain to contenteditable
// -------- Test dragging special text type of text/plain to <input>
selection.selectAllChildren(text);
inputEvents = [];
synthesizeDrop(text, input, [[{type: "text/x-moz-text-internal", data: "Some Special Text"}]], "copy");
is(input.value, "Some Plain Text", "Drag text/x-moz-text-internal onto input");
is(inputEvents.length, 0,
'No "input" event should be fired when dropping text/x-moz-text-internal into <input> element');
// -------- Test dragging regular text of text/plain to contenteditable
selection.selectAllChildren(text);
inputEvents = [];
synthesizeDrop(text, contenteditable, [[{type: "text/plain", data: "Sample Text"}]], "copy");
is(contenteditable.childNodes.length, 3, "Drag text/plain onto contenteditable child nodes");
is(contenteditable.textContent, "This is some editable text.Sample Text",
"Drag text/plain onto contenteditable text");
is(inputEvents.length, 1,
'Only one "input" events should be fired when dropping text/plain into contenteditable element');
checkInputEvent(inputEvents[0], contenteditable, "when dropping text/plain into contenteditable element");
// -------- Test dragging regular text of text/html to contenteditable
selection.selectAllChildren(text);
inputEvents = [];
synthesizeDrop(text, contenteditable, [[{type: "text/html", data: "Sample <i>Italic</i> Text"}]], "copy");
is(contenteditable.childNodes.length, 6, "Drag text/html onto contenteditable child nodes");
is(contenteditable.childNodes[4].tagName, "I", "Drag text/html onto contenteditable italic");
is(contenteditable.childNodes[4].textContent, "Italic", "Drag text/html onto contenteditable italic text");
is(inputEvents.length, 1,
'Only one "input" events should be fired when dropping text/html into contenteditable element');
checkInputEvent(inputEvents[0], contenteditable, "when dropping text/html into contenteditable element");
// -------- Test dragging contenteditable to <input>
selection.selectAllChildren(document.getElementById("bold"));
inputEvents = [];
synthesizeDrop(bold, input, [[{type: "text/html", data: "<b>editable</b>"},
{type: "text/plain", data: "editable"}]], "copy");
is(input.value, "Some Plain Texteditable", "Move text/html and text/plain from contenteditable onto input");
is(input.value, "Some Plain Texteditable", "Copy text/html and text/plain from contenteditable onto input");
is(inputEvents.length, 1,
'Only one "input" events should be fired when dragging from contenteditable to <input> element');
checkInputEvent(inputEvents[0], input, "when dragging from contenteditable to <input> element");
// -------- Test dragging contenteditable to contenteditable
shouldClear = false;
selection.selectAllChildren(contenteditable.childNodes[4]);
inputEvents = [];
synthesizeDrop(contenteditable.childNodes[4], contenteditable, [], "copy");
is(contenteditable.childNodes.length, 7, "Move text/html and text/plain from contenteditable onto itself child nodes");
is(contenteditable.childNodes[6].tagName, "I", "Move text/html and text/plain from contenteditable onto itself italic");
is(contenteditable.childNodes[6].textContent, "Italic", "Move text/html and text/plain from contenteditable onto itself text");
is(inputEvents.length, 1,
'Only one "input" events should be fired when dragging (copy) in a contentediable element');
checkInputEvent(inputEvents[0], contenteditable, "when dragging (copy) in a contentediable element");
// We'd test 'move' here as well as 'copy', but that requires knowledge of
// the source of the drag which drag simulation doesn't provide.
@ -165,9 +226,17 @@ function doTest() {
var nonEditable = document.getElementById("noneditable");
selection.selectAllChildren(nonEditable);
inputEvents = [];
synthesizeDrop(nonEditable, document.getElementById("first"), [], "copy");
is(document.getElementById("nestedce").textContent, " MiddleFirst letter Middle Last part",
"Drag non-editable text/html onto contenteditable text");
todo_is(inputEvents.length, 1,
'Only one "input" events should be fired when dragging from inner non-editable element to a contentediable element');
if (inputEvents.length > 0) {
checkInputEvent(inputEvents[0], document.getElementById("nestedce"), "when dragging from inner non-editable element to a contentediable element");
}
document.removeEventListener("input", onInput);
SimpleTest.finish();
}

View File

@ -76,37 +76,68 @@ async function copyHTMLContent(aInnerHTML) {
});
}
function checkInputEvent(aEvent, aDescription) {
ok(aEvent instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface ${aDescription}`);
is(aEvent.cancelable, false,
`"input" event should be never cancelable ${aDescription}`);
is(aEvent.bubbles, true,
`"input" event should always bubble ${aDescription}`);
}
async function doTextareaTests(aTextarea) {
let inputEvents = [];
function onInput(aEvent) {
inputEvents.push(aEvent);
}
aTextarea.addEventListener("input", onInput);
await copyPlaintext("abc\ndef\nghi");
aTextarea.focus();
inputEvents = [];
synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
is(aTextarea.value,
"> abc\n> def\n> ghi\n\n",
"Pasted each line should start with \"> \"");
is(inputEvents.length, 1,
'One "input" event should be fired #1');
checkInputEvent(inputEvents[0], "#1");
aTextarea.value = "";
await copyPlaintext("> abc\n> def\n> ghi");
aTextarea.focus();
inputEvents = [];
synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
is(aTextarea.value,
">> abc\n>> def\n>> ghi\n\n",
"Pasted each line should be start with \">> \" when already quoted one level");
is(inputEvents.length, 1,
'One "input" event should be fired #2');
checkInputEvent(inputEvents[0], "#2");
aTextarea.value = "";
await copyPlaintext("> abc\n> def\n\nghi");
aTextarea.focus();
inputEvents = [];
synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
is(aTextarea.value,
">> abc\n>> def\n> \n> ghi\n\n",
"Pasted each line should be start with \">> \" when already quoted one level");
is(inputEvents.length, 1,
'One "input" event should be fired #3');
checkInputEvent(inputEvents[0], "#3");
aTextarea.value = "";
await copyPlaintext("abc\ndef\n\n");
aTextarea.focus();
inputEvents = [];
synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true});
is(aTextarea.value,
"> abc\n> def\n> \n",
"If pasted text ends with \"\\n\", only the last line should not started with \">\"");
is(inputEvents.length, 1,
'One "input" event should be fired #4');
checkInputEvent(inputEvents[0], "#4");
aTextarea.value = "";
let pasteEventCount = 0;
@ -118,56 +149,81 @@ async function doTextareaTests(aTextarea) {
await copyPlaintext("abc");
aTextarea.focus();
document.body.addEventListener("click", (event) => { event.preventDefault(); }, {capture: true, once: true});
inputEvents = [];
synthesizeMouseAtCenter(aTextarea, {button: 1});
is(aTextarea.value, "",
"If 'click' event is consumed at capturing phase of the <body>, paste should be canceled");
is(pasteEventCount, 0,
"If 'click' event is consumed at capturing phase of the <body>, 'paste' event should not be fired");
is(inputEvents.length, 0,
'No "input" event should be fired when the "click" event is canceled');
aTextarea.value = "";
await copyPlaintext("abc");
aTextarea.focus();
aTextarea.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true});
pasteEventCount = 0;
inputEvents = [];
synthesizeMouseAtCenter(aTextarea, {button: 1});
is(aTextarea.value, "abc",
"Even if 'mouseup' event is consumed, paste should be done");
is(pasteEventCount, 1,
"Even if 'mouseup' event is consumed, 'paste' event should be fired once");
is(inputEvents.length, 1,
'One "input" event should be fired even if "mouseup" event is canceled');
checkInputEvent(inputEvents[0], 'even if "mouseup" event is canceled');
aTextarea.value = "";
await copyPlaintext("abc");
aTextarea.focus();
aTextarea.addEventListener("click", (event) => { event.preventDefault(); }, {once: true});
pasteEventCount = 0;
inputEvents = [];
synthesizeMouseAtCenter(aTextarea, {button: 1});
is(aTextarea.value, "abc",
"Even if 'click' event handler is added to the <textarea>, paste should not be canceled");
is(pasteEventCount, 1,
"Even if 'click' event handler is added to the <textarea>, 'paste' event should be fired once");
is(inputEvents.length, 1,
'One "input" event should be fired even if "click" event is canceled in bubbling phase');
checkInputEvent(inputEvents[0], 'even if "click" event is canceled in bubbling phase');
aTextarea.value = "";
await copyPlaintext("abc");
aTextarea.focus();
aTextarea.addEventListener("auxclick", (event) => { event.preventDefault(); }, {once: true});
pasteEventCount = 0;
inputEvents = [];
synthesizeMouseAtCenter(aTextarea, {button: 1});
todo_is(aTextarea.value, "",
"If 'auxclick' event is consumed, paste should be canceled");
todo_is(pasteEventCount, 0,
"If 'auxclick' event is consumed, 'paste' event should not be fired once");
todo_is(inputEvents.length, 0,
'No "input" event should be fired if "auxclick" event is canceled');
aTextarea.value = "";
aTextarea.removeEventListener("paste", pasteEventLogger);
aTextarea.removeEventListener("input", onInput);
}
async function doContenteditableTests(aEditableDiv) {
let inputEvents = [];
function onInput(aEvent) {
inputEvents.push(aEvent);
}
aEditableDiv.addEventListener("input", onInput);
await copyPlaintext("abc\ndef\nghi");
aEditableDiv.focus();
inputEvents = [];
synthesizeMouseAtCenter(aEditableDiv, {button: 1, ctrlKey: true});
is(aEditableDiv.innerHTML,
"<blockquote type=\"cite\">abc<br>def<br>ghi</blockquote>",
"Pasted plaintext should be in <blockquote> element and each linebreaker should be <br> element");
is(inputEvents.length, 1,
'One "input" event should be fired on the editing host');
checkInputEvent(inputEvents[0], "(contenteditable)");
aEditableDiv.innerHTML = "";
let pasteEventCount = 0;
@ -179,55 +235,71 @@ async function doContenteditableTests(aEditableDiv) {
await copyPlaintext("abc");
aEditableDiv.focus();
window.addEventListener("click", (event) => { event.preventDefault(); }, {capture: true, once: true});
inputEvents = [];
synthesizeMouseAtCenter(aEditableDiv, {button: 1});
is(aEditableDiv.innerHTML, "",
"If 'click' event is consumed at capturing phase of the window, paste should be canceled");
is(pasteEventCount, 0,
"If 'click' event is consumed at capturing phase of the window, 'paste' event should be fired once");
is(inputEvents.length, 0,
'No "input" event should be fired when the "click" event is canceled (contenteditable)');
aEditableDiv.innerHTML = "";
await copyPlaintext("abc");
aEditableDiv.focus();
aEditableDiv.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true});
pasteEventCount = 0;
inputEvents = [];
synthesizeMouseAtCenter(aEditableDiv, {button: 1});
is(aEditableDiv.innerHTML, "abc",
"Even if 'mouseup' event is consumed, paste should be done");
is(pasteEventCount, 1,
"Even if 'mouseup' event is consumed, 'paste' event should be fired once");
is(inputEvents.length, 1,
'One "input" event should be fired even if "mouseup" event is canceled (contenteditable)');
checkInputEvent(inputEvents[0], 'even if "mouseup" event is canceled (contenteditable)');
aEditableDiv.innerHTML = "";
await copyPlaintext("abc");
aEditableDiv.focus();
aEditableDiv.addEventListener("click", (event) => { event.preventDefault(); }, {once: true});
pasteEventCount = 0;
inputEvents = [];
synthesizeMouseAtCenter(aEditableDiv, {button: 1});
is(aEditableDiv.innerHTML, "abc",
"Even if 'click' event handler is added to the editing host, paste should not be canceled");
is(pasteEventCount, 1,
"Even if 'click' event handler is added to the editing host, 'paste' event should be fired");
is(inputEvents.length, 1,
'One "input" event should be fired even if "click" event is canceled in bubbling phase (contenteditable)');
checkInputEvent(inputEvents[0], 'even if "click" event is canceled in bubbling phase (contenteditable)');
aEditableDiv.innerHTML = "";
await copyPlaintext("abc");
aEditableDiv.focus();
aEditableDiv.addEventListener("auxclick", (event) => { event.preventDefault(); }, {once: true});
pasteEventCount = 0;
inputEvents = [];
synthesizeMouseAtCenter(aEditableDiv, {button: 1});
todo_is(aEditableDiv.innerHTML, "",
"If 'auxclick' event is consumed, paste should be canceled");
todo_is(pasteEventCount, 0,
"If 'auxclick' event is consumed, 'paste' event should not be fired");
todo_is(inputEvents.length, 0,
'No "input" event should be fired if "auxclick" event is canceled (contenteditable)');
aEditableDiv.innerHTML = "";
aEditableDiv.removeEventListener("paste", pasteEventLogger);
// Oddly, copyHTMLContent fails randomly only on Linux. Let's skip this.
if (navigator.platform.startsWith("Linux")) {
aEditableDiv.removeEventListener("input", onInput);
return;
}
await copyHTMLContent("<p>abc</p><p>def</p><p>ghi</p>");
aEditableDiv.focus();
inputEvents = [];
synthesizeMouseAtCenter(aEditableDiv, {button: 1, ctrlKey: true});
if (!navigator.appVersion.includes("Android")) {
is(aEditableDiv.innerHTML,
@ -239,7 +311,12 @@ async function doContenteditableTests(aEditableDiv) {
"<blockquote type=\"cite\">abc<br><br>def<br><br>ghi</blockquote>",
"Pasted HTML content should be set to the <blockquote>");
}
is(inputEvents.length, 1,
'One "input" event should be fired when pasting HTML');
checkInputEvent(inputEvents[0], "when pasting HTML");
aEditableDiv.innerHTML = "";
aEditableDiv.removeEventListener("input", onInput);
}
async function doNestedEditorTests(aEditableDiv) {

View File

@ -19,6 +19,12 @@ SimpleTest.waitForFocus(function() {
let editor = document.getElementById("content");
let selection = document.getSelection();
let inputEvents = [];
function onInput(aEvent) {
inputEvents.push(aEvent);
}
editor.addEventListener("input", onInput);
editor.focus();
selection.collapse(editor, 0);
@ -26,6 +32,7 @@ SimpleTest.waitForFocus(function() {
getEditor().flags |= SpecialPowers.Ci.nsIPlaintextEditor.eEditorPlaintextMask;
inputEvents = [];
getEditorMailSupport().insertAsCitedQuotation("this is quoted text\nAnd here is second line.", "this is cited text", false);
ok(selection.isCollapsed,
@ -36,12 +43,21 @@ SimpleTest.waitForFocus(function() {
"focus offset of Selection should be next to inserted <span> element after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor");
is(editor.innerHTML, '<span style="white-space: pre-wrap;">&gt; this is quoted text<br>&gt; And here is second line.<br><br></span>',
"The quoted text should be inserted as plaintext into the plaintext editor");
is(inputEvents.length, 1,
'One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor');
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor');
// Tests when the editor is in HTML editor mode.
getEditor().flags &= ~SpecialPowers.Ci.nsIPlaintextEditor.eEditorPlaintextMask;
editor.innerHTML = "";
inputEvents = [];
getEditorMailSupport().insertAsCitedQuotation("this is quoted text<br>", "this is cited text", false);
ok(selection.isCollapsed,
@ -52,9 +68,18 @@ SimpleTest.waitForFocus(function() {
"focus offset of Selection should be next to inserted <span> element after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)");
is(editor.innerHTML,
'<blockquote type="cite" cite="this is cited text">this is quoted text&lt;br&gt;</blockquote>', "The quoted text should be inserted as plaintext into the HTML editor");
is(inputEvents.length, 1,
'One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)');
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)');
editor.innerHTML = "";
inputEvents = [];
getEditorMailSupport().insertAsCitedQuotation("this is quoted text<br>And here is second line.", "this is cited text", true);
ok(selection.isCollapsed,
@ -65,6 +90,14 @@ SimpleTest.waitForFocus(function() {
"focus offset of Selection should be next to inserted <span> element after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)");
is(editor.innerHTML, '<blockquote type="cite" cite="this is cited text">this is quoted text<br>And here is second line.</blockquote>',
"The quoted text should be inserted as HTML source into the HTML editor");
is(inputEvents.length, 1,
'One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)');
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)');
SimpleTest.finish();
});

View File

@ -22,31 +22,37 @@ SimpleTest.waitForFocus(function() {
let textarea = document.getElementsByTagName("textarea")[0];
let contenteditable = document.getElementById("content");
let selection = window.getSelection();
let inputEventCount = 0;
let inputEvents = [];
function onInput(event) {
inputEventCount++;
inputEvents.push(event);
}
input.focus();
input.selectionStart = input.selectionEnd = 3;
inputEventCount = 0;
inputEvents = [];
input.addEventListener("input", onInput);
try {
getPlaintextEditor(input).insertLineBreak();
} catch (e) {}
input.removeEventListener("input", onInput);
is(input.value, "abcdef", "nsIPlaintextEditor.insertLineBreak() should do nothing on single line editor");
is(inputEventCount, 0, "nsIPlaintextEditor.insertLineBreak() shouldn't cause 'input' event on single line editor");
is(inputEvents.length, 0, "nsIPlaintextEditor.insertLineBreak() shouldn't cause 'input' event on single line editor");
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = 3;
inputEventCount = 0;
inputEvents = [];
textarea.addEventListener("input", onInput);
getPlaintextEditor(textarea).insertLineBreak();
textarea.removeEventListener("input", onInput);
is(textarea.value, "abc\ndef", "nsIPlaintextEditor.insertLineBreak() should insert \n into multi-line editor");
is(inputEventCount, 1, "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on multi-line editor");
is(inputEvents.length, 1, "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on multi-line editor");
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface (on multi-line editor)');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event (on multi-line editor)');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble (on multi-line editor)');
// Note that despite of the name, insertLineBreak() should insert paragraph separator in HTMLEditor.
@ -56,53 +62,77 @@ SimpleTest.waitForFocus(function() {
contenteditable.focus();
contenteditable.scrollTop;
selection.collapse(contenteditable.firstChild, 3);
inputEventCount = 0;
inputEvents = [];
contenteditable.addEventListener("input", onInput);
getPlaintextEditor(contenteditable).insertLineBreak();
contenteditable.removeEventListener("input", onInput);
is(contenteditable.innerHTML, "abc<br>def",
"nsIPlaintextEditor.insertLineBreak() should insert <br> element into text node when defaultParagraphSeparator is \"br\"");
is(inputEventCount, 1,
is(inputEvents.length, 1,
"nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has only text node when defaultParagraphSeparator is \"br\"");
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "br") #1');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "br") #1');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble (when defaultParagraphSeparator is "br") #1');
contenteditable.innerHTML = "<p>abcdef</p>";
contenteditable.focus();
contenteditable.scrollTop;
selection.collapse(contenteditable.firstChild.firstChild, 3);
inputEventCount = 0;
inputEvents = [];
contenteditable.addEventListener("input", onInput);
getPlaintextEditor(contenteditable).insertLineBreak();
contenteditable.removeEventListener("input", onInput);
is(contenteditable.innerHTML, "<p>abc</p><p>def</p>",
"nsIPlaintextEditor.insertLineBreak() should add <p> element after <p> element even when defaultParagraphSeparator is \"br\"");
is(inputEventCount, 1,
is(inputEvents.length, 1,
"nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <p> element when defaultParagraphSeparator is \"br\"");
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "br") #2');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "br") #2');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble (when defaultParagraphSeparator is "br") #2');
contenteditable.innerHTML = "<div>abcdef</div>";
contenteditable.focus();
contenteditable.scrollTop;
selection.collapse(contenteditable.firstChild.firstChild, 3);
inputEventCount = 0;
inputEvents = [];
contenteditable.addEventListener("input", onInput);
getPlaintextEditor(contenteditable).insertLineBreak();
contenteditable.removeEventListener("input", onInput);
is(contenteditable.innerHTML, "<div>abc<br>def</div>",
"nsIPlaintextEditor.insertLineBreak() should insert <br> element into <div> element when defaultParagraphSeparator is \"br\"");
is(inputEventCount, 1,
is(inputEvents.length, 1,
"nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <div> element when defaultParagraphSeparator is \"br\"");
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "br") #3');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "br") #3');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble (when defaultParagraphSeparator is "br") #3');
contenteditable.innerHTML = "<pre>abcdef</pre>";
contenteditable.focus();
contenteditable.scrollTop;
selection.collapse(contenteditable.firstChild.firstChild, 3);
inputEventCount = 0;
inputEvents = [];
contenteditable.addEventListener("input", onInput);
getPlaintextEditor(contenteditable).insertLineBreak();
contenteditable.removeEventListener("input", onInput);
is(contenteditable.innerHTML, "<pre>abc<br>def</pre>",
"nsIPlaintextEditor.insertLineBreak() should insert <br> element into <pre> element when defaultParagraphSeparator is \"br\"");
is(inputEventCount, 1,
is(inputEvents.length, 1,
"nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <pre> element when defaultParagraphSeparator is \"br\"");
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "br") #4');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "br") #4');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble (when defaultParagraphSeparator is "br") #4');
document.execCommand("defaultParagraphSeparator", false, "p");
@ -110,53 +140,77 @@ SimpleTest.waitForFocus(function() {
contenteditable.focus();
contenteditable.scrollTop;
selection.collapse(contenteditable.firstChild, 3);
inputEventCount = 0;
inputEvents = [];
contenteditable.addEventListener("input", onInput);
getPlaintextEditor(contenteditable).insertLineBreak();
contenteditable.removeEventListener("input", onInput);
is(contenteditable.innerHTML, "<p>abc</p><p>def</p>",
"nsIPlaintextEditor.insertLineBreak() should create <p> elements when there is only text node and defaultParagraphSeparator is \"p\"");
is(inputEventCount, 1,
is(inputEvents.length, 1,
"nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has only text node when defaultParagraphSeparator is \"p\"");
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "p") #1');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "p") #1');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble (when defaultParagraphSeparator is "p") #1');
contenteditable.innerHTML = "<p>abcdef</p>";
contenteditable.focus();
contenteditable.scrollTop;
selection.collapse(contenteditable.firstChild.firstChild, 3);
inputEventCount = 0;
inputEvents = [];
contenteditable.addEventListener("input", onInput);
getPlaintextEditor(contenteditable).insertLineBreak();
contenteditable.removeEventListener("input", onInput);
is(contenteditable.innerHTML, "<p>abc</p><p>def</p>",
"nsIPlaintextEditor.insertLineBreak() should add <p> element after <p> element when defaultParagraphSeparator is \"p\"");
is(inputEventCount, 1,
is(inputEvents.length, 1,
"nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <p> element when defaultParagraphSeparator is \"p\"");
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "p") #2');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "p") #2');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble (when defaultParagraphSeparator is "p") #2');
contenteditable.innerHTML = "<div>abcdef</div>";
contenteditable.focus();
contenteditable.scrollTop;
selection.collapse(contenteditable.firstChild.firstChild, 3);
inputEventCount = 0;
inputEvents = [];
contenteditable.addEventListener("input", onInput);
getPlaintextEditor(contenteditable).insertLineBreak();
contenteditable.removeEventListener("input", onInput);
is(contenteditable.innerHTML, "<div>abc</div><div>def</div>",
"nsIPlaintextEditor.insertLineBreak() should add <div> element after <div> element even when defaultParagraphSeparator is \"p\"");
is(inputEventCount, 1,
is(inputEvents.length, 1,
"nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <div> element when defaultParagraphSeparator is \"p\"");
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "p") #3');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "p") #3');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble (when defaultParagraphSeparator is "p") #3');
contenteditable.innerHTML = "<pre>abcdef</pre>";
contenteditable.focus();
contenteditable.scrollTop;
selection.collapse(contenteditable.firstChild.firstChild, 3);
inputEventCount = 0;
inputEvents = [];
contenteditable.addEventListener("input", onInput);
getPlaintextEditor(contenteditable).insertLineBreak();
contenteditable.removeEventListener("input", onInput);
is(contenteditable.innerHTML, "<pre>abc<br>def</pre>",
"nsIPlaintextEditor.insertLineBreak() should insert <br> element into <pre> element when defaultParagraphSeparator is \"p\"");
is(inputEventCount, 1,
is(inputEvents.length, 1,
"nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <pre> element when defaultParagraphSeparator is \"p\"");
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "p") #4');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "p") #4');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble (when defaultParagraphSeparator is "p") #4');
document.execCommand("defaultParagraphSeparator", false, "div");
@ -164,53 +218,77 @@ SimpleTest.waitForFocus(function() {
contenteditable.focus();
contenteditable.scrollTop;
selection.collapse(contenteditable.firstChild, 3);
inputEventCount = 0;
inputEvents = [];
contenteditable.addEventListener("input", onInput);
getPlaintextEditor(contenteditable).insertLineBreak();
contenteditable.removeEventListener("input", onInput);
is(contenteditable.innerHTML, "<div>abc</div><div>def</div>",
"nsIPlaintextEditor.insertLineBreak() should create <div> elements when there is only text node and defaultParagraphSeparator is \"div\"");
is(inputEventCount, 1,
is(inputEvents.length, 1,
"nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has only text node when defaultParagraphSeparator is \"div\"");
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "div") #1');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "div") #1');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble (when defaultParagraphSeparator is "div") #1');
contenteditable.innerHTML = "<p>abcdef</p>";
contenteditable.focus();
contenteditable.scrollTop;
selection.collapse(contenteditable.firstChild.firstChild, 3);
inputEventCount = 0;
inputEvents = [];
contenteditable.addEventListener("input", onInput);
getPlaintextEditor(contenteditable).insertLineBreak();
contenteditable.removeEventListener("input", onInput);
is(contenteditable.innerHTML, "<p>abc</p><p>def</p>",
"nsIPlaintextEditor.insertLineBreak() should add <p> element after <p> element even when defaultParagraphSeparator is \"div\"");
is(inputEventCount, 1,
is(inputEvents.length, 1,
"nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <p> element when defaultParagraphSeparator is \"div\"");
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "div") #2');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "div") #2');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble (when defaultParagraphSeparator is "div") #2');
contenteditable.innerHTML = "<div>abcdef</div>";
contenteditable.focus();
contenteditable.scrollTop;
selection.collapse(contenteditable.firstChild.firstChild, 3);
inputEventCount = 0;
inputEvents = [];
contenteditable.addEventListener("input", onInput);
getPlaintextEditor(contenteditable).insertLineBreak();
contenteditable.removeEventListener("input", onInput);
is(contenteditable.innerHTML, "<div>abc</div><div>def</div>",
"nsIPlaintextEditor.insertLineBreak() should add <div> element after <div> element when defaultParagraphSeparator is \"div\"");
is(inputEventCount, 1,
is(inputEvents.length, 1,
"nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <div> element when defaultParagraphSeparator is \"div\"");
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "div") #3');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "div") #3');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble (when defaultParagraphSeparator is "div") #3');
contenteditable.innerHTML = "<pre>abcdef</pre>";
contenteditable.focus();
contenteditable.scrollTop;
selection.collapse(contenteditable.firstChild.firstChild, 3);
inputEventCount = 0;
inputEvents = [];
contenteditable.addEventListener("input", onInput);
getPlaintextEditor(contenteditable).insertLineBreak();
contenteditable.removeEventListener("input", onInput);
is(contenteditable.innerHTML, "<pre>abc<br>def</pre>",
"nsIPlaintextEditor.insertLineBreak() should insert <br> element into <pre> element when defaultParagraphSeparator is \"div\"");
is(inputEventCount, 1,
is(inputEvents.length, 1,
"nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <pre> element when defaultParagraphSeparator is \"div\"");
ok(inputEvents[0] instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "div") #4');
is(inputEvents[0].cancelable, false,
'"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "div") #4');
is(inputEvents[0].bubbles, true,
'"input" event should always bubble (when defaultParagraphSeparator is "div") #4');
SimpleTest.finish();
});

View File

@ -19,17 +19,38 @@ SimpleTest.waitForFocus(function() {
let editor = document.getElementById("content");
let selection = document.getSelection();
function checkInputEvent(aEvent, aDescription) {
ok(aEvent instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface ${aDescription}`);
is(aEvent.cancelable, false,
`"input" event should be never cancelable ${aDescription}`);
is(aEvent.bubbles, true,
`"input" event should always bubble ${aDescription}`);
}
let inputEvents = [];
function onInput(aEvent) {
inputEvents.push(aEvent);
}
editor.addEventListener("input", onInput);
inputEvents = [];
selection.collapse(editor.firstChild, 0);
getTableEditor().deleteTableCell(1);
is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
"nsITableEditor.deleteTableCell(1) should do nothing if selection is not in <table>");
is(inputEvents.length, 0,
'No "input" event should be fired when a call of nsITableEditor.deleteTableCell(1) does nothing');
selection.removeAllRanges();
try {
inputEvents = [];
getTableEditor().deleteTableCell(1);
ok(false, "getTableEditor().deleteTableCell(1) without selection ranges should throw exception");
} catch (e) {
ok(true, "getTableEditor().deleteTableCell(1) without selection ranges should throw exception");
is(inputEvents.length, 0,
'No "input" event should be fired when nsITableEditor.deleteTableCell(1) causes exception due to no selection range');
}
selection.removeAllRanges();
@ -37,6 +58,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</table>";
inputEvents = [];
let range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -46,12 +68,16 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should remove only selected cell when only one cell is selected #1-1");
is(inputEvents.length, 1,
'Only one "input" event should be fired when only one cell is selected #1-1');
checkInputEvent(inputEvents[0], "when only one cell is selected #1-1");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
'<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' +
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -61,12 +87,16 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should remove only selected cell when only one cell is selected #1-2");
is(inputEvents.length, 1,
'Only one "input" event should be fired when only one cell is selected #1-2');
checkInputEvent(inputEvents[0], "when only one cell is selected #1-2");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
'<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td></tr>' +
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -76,6 +106,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should remove only selected cell when only one cell is selected #1-3");
is(inputEvents.length, 1,
'Only one "input" event should be fired when only one cell is selected #1-3');
checkInputEvent(inputEvents[0], "when only one cell is selected #1-3");
// When only one cell element is selected, the argument should be used.
selection.removeAllRanges();
@ -83,6 +116,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -91,7 +125,10 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell1-3</td></tr>" +
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(2) should remove selected cell element and next cell element in same row");
"nsITableEditor.deleteTableCellContents(2) should remove selected cell element and next cell element in same row #1-4");
is(inputEvents.length, 1,
'Only one "input" event should be fired when only one cell is selected #1-4');
checkInputEvent(inputEvents[0], "when only one cell is selected #1-4");
// When the argument is larger than remaining cell elements from selected
// cell element, the behavior is really buggy.
@ -100,6 +137,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td></tr>' +
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -109,6 +147,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(2) should remove selected cell element and its previous cell element when it reaches the last cell element in the row");
is(inputEvents.length, 1,
'Only one "input" event should be fired when the argument is larger than remaining cell elements from selected cell element');
checkInputEvent(inputEvents[0], "when the argument is larger than remaining cell elements from selected cell element");
// XXX If the former case is expected, first row should be removed in this
// case, but it removes only selected cell and its previous cell.
@ -117,6 +158,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td></tr>' +
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -125,6 +167,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(4) should remove the first row when a cell in it is selected and the argument is larger than number of cells in the row");
is(inputEvents.length, 1,
'Only one "input" event should be fired when the argument is larger than number of cells in the row');
checkInputEvent(inputEvents[0], "when the argument is larger than number of cells in the row");
// If 2 or more cells are selected, the argument should be ignored.
selection.removeAllRanges();
@ -132,6 +177,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select1">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' +
'<tr><td>cell2-1</td><td id="select2">cell2-2</td><td>cell2-3</td></tr>' +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -144,6 +190,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-3</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should remove selected cell elements even if the argument is smaller than number of selected cells");
is(inputEvents.length, 1,
'Only one "input" event should be fired even if the argument is smaller than number of selected cells');
checkInputEvent(inputEvents[0], "even if the argument is smaller than number of selected cells");
// If all cells in a row are selected, the <tr> element should also be removed.
selection.removeAllRanges();
@ -151,6 +200,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td><td id="select3">cell1-3</td></tr>' +
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -165,12 +215,16 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should remove also <tr> element when all cell elements in a row is selected");
is(inputEvents.length, 1,
'Only one "input" event should be fired when all cell elements in a row is selected');
checkInputEvent(inputEvents[0], "when all cell elements in a row is selected");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
'<tr><td id="select">cell1-1</td></tr>' +
"<tr><td>cell2-1</td></tr>" +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -179,6 +233,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should remove also <tr> element when a cell element which is only child of a row is selected");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell element which is only child of a row is selected');
checkInputEvent(inputEvents[0], "when a cell element which is only child of a row is selected");
// If all cells are removed, the <table> element should be removed.
selection.removeAllRanges();
@ -186,6 +243,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td></tr>' +
'<tr><td id="select3">cell2-1</td><td id="select4">cell2-2</td></tr>' +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -201,17 +259,24 @@ SimpleTest.waitForFocus(function() {
getTableEditor().deleteTableCell(1);
is(editor.innerHTML, "",
"nsITableEditor.deleteTableCellContents(1) should remove also <table> element when all cell elements are selected");
is(inputEvents.length, 1,
'Only one "input" event should be fired when all cell elements are selected');
checkInputEvent(inputEvents[0], "when all cell elements are selected");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
'<tr><td id="select">cell1-1</td></tr>' +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
getTableEditor().deleteTableCell(1);
is(editor.innerHTML, "",
"nsITableEditor.deleteTableCellContents(1) should remove also <table> element when a cell element which is only child of <table> is selected");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell element which is only child of <table> is selected');
checkInputEvent(inputEvents[0], "when a cell element which is only child of <table> is selected");
// rowspan
selection.removeAllRanges();
@ -220,6 +285,7 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-2</td></tr>" +
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -230,6 +296,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning rows");
is(inputEvents.length, 1,
'Only one "input" event should be fired when removing the cell spanning rows');
checkInputEvent(inputEvents[0], "when removing the cell spanning rows");
// XXX cell3-1 is also removed even though it's not selected.
selection.removeAllRanges();
@ -238,6 +307,7 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-2</td></tr>" +
'<tr><td>cell3-1</td><td id="select2">cell3-2</td></tr>' +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -251,6 +321,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning rows (when 2 cell elements are selected)");
is(inputEvents.length, 1,
'Only one "input" event should be fired when removing the cell spanning rows (when 2 cell elements are selected)');
checkInputEvent(inputEvents[0], "when removing the cell spanning rows (when 2 cell elements are selected)");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -258,6 +331,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select">cell2-2</td></tr>' +
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -267,6 +341,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element is removed");
is(inputEvents.length, 1,
"Only one \"input\" event should be fired when spanned <tr>'s last child cell element is removed");
checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element is removed");
// XXX broken case, the second row isn't removed.
selection.removeAllRanges();
@ -275,6 +352,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select1">cell2-2</td></tr>' +
'<tr><td>cell3-1</td><td id="select2">cell3-2</td></tr>' +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -287,6 +365,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element is removed (when 2 cell elements are selected)");
is(inputEvents.length, 1,
"Only one \"input\" event should be fired when spanned <tr>'s last child cell element is removed (when 2 cell elements are selected)");
checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element is removed (when 2 cell elements are selected)");
// XXX broken case, neither the selected cell nor the second row is removed.
selection.removeAllRanges();
@ -295,6 +376,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select">cell2-2</td></tr>' +
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -304,6 +386,11 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element is removed (removing middle row)");
todo_is(inputEvents.length, 1,
"Only one \"input\" event should be fired when spanned <tr>'s last child cell element is removed (removing middle row)");
if (inputEvents.length > 0) {
checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element is removed (removing middle row)");
}
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -311,6 +398,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select1">cell2-2</td></tr>' +
'<tr><td>cell3-1</td><td id="select2">cell3-2</td></tr>' +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -322,6 +410,9 @@ SimpleTest.waitForFocus(function() {
'<tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr>' +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element and remove the last row");
is(inputEvents.length, 1,
"Only one \"input\" event should be fired when spanned <tr>'s last child cell element and remove the last row");
checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element and remove the last row");
// colspan
selection.removeAllRanges();
@ -329,6 +420,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select" colspan="2">cell1-1</td><td>cell1-3</td></tr>' +
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -338,12 +430,16 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning columns");
is(inputEvents.length, 1,
'Only one "input" event should be fired when removing the cell spanning columns');
checkInputEvent(inputEvents[0], "when removing the cell spanning columns");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
'<tr><td id="select1" colspan="2">cell1-1</td><td>cell1-3</td></tr>' +
'<tr><td>cell2-1</td><td id="select2">cell2-2</td><td>cell2-3</td></tr>' +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -356,6 +452,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-3</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning columns and the other selected cell element");
is(inputEvents.length, 1,
'Only one "input" event should be fired when removing the cell spanning columns and the other selected cell element');
checkInputEvent(inputEvents[0], "when removing the cell spanning columns and the other selected cell element");
// XXX broken case, colspan is not adjusted.
selection.removeAllRanges();
@ -363,6 +462,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td colspan="2">cell1-1</td><td>cell1-3</td></tr>' +
'<tr><td>cell2-1</td><td id="select">cell2-2</td><td>cell2-3</td></tr>' +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -372,12 +472,16 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-3</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should adjust different row's colspan when corresponding cell element is removed");
is(inputEvents.length, 1,
'Only one "input" event should be fired when corresponding cell element is removed');
checkInputEvent(inputEvents[0], "when corresponding cell element is removed");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
'<tr><td colspan="2">cell1-1</td><td>cell1-3</td></tr>' +
'<tr><td>cell2-1</td><td id="select1">cell2-2</td><td id="select2">cell2-3</td></tr>' +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -390,12 +494,16 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should adjust different row's colspan when corresponding cell element is removed (when 2 cell elements are selected)");
is(inputEvents.length, 1,
'Only one "input" event should be fired when corresponding cell element is removed (when 2 cell elements are selected)');
checkInputEvent(inputEvents[0], "when corresponding cell element is removed (when 2 cell elements are selected)");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
'<tr><td>cell1-1</td><td id="select1">cell1-2</td><td>cell1-4</td></tr>' +
'<tr><td id="select2" colspan="2">cell2-1</td><td>cell2-3</td><td>cell2-4</td></tr>' +
"</table>";
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -408,6 +516,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-3</td><td>cell2-4</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should adjust different row's colspan when corresponding cell element is removed (when 2 cell elements are selected)");
is(inputEvents.length, 1,
'Only one "input" event should be fired when corresponding cell element is removed (when 2 cell elements are selected)');
checkInputEvent(inputEvents[0], "when corresponding cell element is removed (when 2 cell elements are selected)");
// When a cell contains first selection range, it should be removed.
selection.removeAllRanges();
@ -416,6 +527,7 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</table>";
editor.scrollTop; // requires layout information.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 1);
getTableEditor().deleteTableCell(1);
@ -424,6 +536,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(1) should remove only a cell containing first selection range when there is no selected cell element");
is(inputEvents.length, 1,
'Only one "input" event should be fired when there is no selected cell element');
checkInputEvent(inputEvents[0], "when there is no selected cell element");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -431,6 +546,7 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</table>";
editor.scrollTop; // requires layout information.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 1);
getTableEditor().deleteTableCell(2);
@ -439,6 +555,11 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents(2) should remove only 2 cell elements starting from a cell containing first selection range when there is no selected cell element");
is(inputEvents.length, 1,
'Only one "input" event should be fired when there is no selected cell element');
checkInputEvent(inputEvents[0], "when there is no selected cell element");
editor.removeEventListener("input", onInput);
SimpleTest.finish();
});

View File

@ -19,17 +19,38 @@ SimpleTest.waitForFocus(function() {
let editor = document.getElementById("content");
let selection = document.getSelection();
function checkInputEvent(aEvent, aDescription) {
ok(aEvent instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface ${aDescription}`);
is(aEvent.cancelable, false,
`"input" event should be never cancelable ${aDescription}`);
is(aEvent.bubbles, true,
`"input" event should always bubble ${aDescription}`);
}
let inputEvents = [];
function onInput(aEvent) {
inputEvents.push(aEvent);
}
editor.addEventListener("input", onInput);
selection.collapse(editor.firstChild, 0);
inputEvents = [];
getTableEditor().deleteTableCellContents();
is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
"nsITableEditor.deleteTableColumn() should do nothing if selection is not in <table>");
"nsITableEditor.deleteTableCellContents() should do nothing if selection is not in <table>");
is(inputEvents.length, 0,
'No "input" event should be fired when a call of nsITableEditor.deleteTableCellContents() does nothing');
selection.removeAllRanges();
try {
inputEvents = [];
getTableEditor().deleteTableCellContents();
ok(false, "getTableEditor().deleteTableCellContents() without selection ranges should throw exception");
} catch (e) {
ok(true, "getTableEditor().deleteTableCellContents() without selection ranges should throw exception");
is(inputEvents.length, 0,
'No "input" event should be fired when nsITableEditor.deleteTableCellContents() causes exception due to no selection range');
}
selection.removeAllRanges();
@ -38,6 +59,7 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
"</table>";
editor.focus();
inputEvents = [];
let range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -47,6 +69,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents() should replace the selected cell's text with <br> element");
is(inputEvents.length, 1,
'Only one "input" event should be fired when all text in a cell is selected');
checkInputEvent(inputEvents[0], "when all text in a cell is selected");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -54,6 +79,7 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
"</table>";
editor.focus();
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -63,6 +89,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents() should replace the selected cell's <ul> element with <br> element");
is(inputEvents.length, 1,
'Only one "input" event should be fired when <ul> element in a cell is selected');
checkInputEvent(inputEvents[0], "when <ul> element in a cell is selected");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -70,6 +99,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select2">cell2-1</td><td>cell2-2</td></tr>' +
"</table>";
editor.focus();
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -82,6 +112,9 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select2"><br></td><td>cell2-2</td></tr>' +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents() should replace the selected 2 cells' text with <br> element");
is(inputEvents.length, 1,
'Only one "input" event should be fired when 2 cell elements are selected');
checkInputEvent(inputEvents[0], "when 2 cell elements are selected");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -89,6 +122,7 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select3">cell2-1</td><td id="select4">cell2-2</td></tr>' +
"</table>";
editor.focus();
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -107,6 +141,9 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select3"><br></td><td id="select4"><br></td></tr>' +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents() should replace the selected 4 cells' text with <br> element");
is(inputEvents.length, 1,
'Only one "input" event should be fired when 4 cell elements are selected');
checkInputEvent(inputEvents[0], "when 4 cell elements are selected");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -114,6 +151,7 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-2</td></tr>" +
"</table>";
editor.focus();
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -123,6 +161,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents() should replace the selected cell's text with <br> element (even if the cell is row-spanning)");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell element are selected (even if the cell is row-spanning)');
checkInputEvent(inputEvents[0], "when a cell element are selected (even if the cell is row-spanning)");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -130,6 +171,7 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
"</table>";
editor.focus();
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
@ -139,6 +181,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents() should replace the selected cell's text with <br> element (even if the cell is column-spanning)");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell element are selected (even if the cell is column-spanning)');
checkInputEvent(inputEvents[0], "when a cell element are selected (even if the cell is column-spanning)");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -147,6 +192,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // Requires layout information.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().deleteTableCellContents();
@ -155,6 +201,11 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.deleteTableCellContents() should replace a cell's text with <br> element when the cell contains selection range");
is(inputEvents.length, 1,
'Only one "input" event should be fired when the cell contains selection range');
checkInputEvent(inputEvents[0], "when the cell contains selection range");
editor.removeEventListener("input", onInput);
SimpleTest.finish();
});

View File

@ -19,17 +19,38 @@ SimpleTest.waitForFocus(function() {
let editor = document.getElementById("content");
let selection = document.getSelection();
function checkInputEvent(aEvent, aDescription) {
ok(aEvent instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface ${aDescription}`);
is(aEvent.cancelable, false,
`"input" event should be never cancelable ${aDescription}`);
is(aEvent.bubbles, true,
`"input" event should always bubble ${aDescription}`);
}
let inputEvents = [];
function onInput(aEvent) {
inputEvents.push(aEvent);
}
editor.addEventListener("input", onInput);
inputEvents = [];
selection.collapse(editor.firstChild, 0);
getTableEditor().deleteTableRow(1);
is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
"nsITableEditor.deleteTableRow(1) should do nothing if selection is not in <table>");
is(inputEvents.length, 0,
'No "input" event should be fired when a call of nsITableEditor.deleteTableRow(1) does nothing');
selection.removeAllRanges();
try {
inputEvents = [];
getTableEditor().deleteTableRow(1);
ok(false, "getTableEditor().deleteTableRow(1) without selection ranges should throw exception");
} catch (e) {
ok(true, "getTableEditor().deleteTableRow(1) without selection ranges should throw exception");
is(inputEvents.length, 0,
'No "input" event should be fired when nsITableEditor.deleteTableRow(1) causes exception due to no selection range');
}
// If a cell is selected and the argument is less than number of rows,
@ -40,62 +61,86 @@ SimpleTest.waitForFocus(function() {
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
inputEvents = [];
let range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
getTableEditor().deleteTableRow(1);
is(editor.innerHTML, "<table><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>",
"nsITableEditor.deleteTableRow(1) should delete the first row when a cell in the first row is selected");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell in the first row is selected');
checkInputEvent(inputEvents[0], "when a cell in the first row is selected");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr></table>';
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
getTableEditor().deleteTableRow(1);
is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
"nsITableEditor.deleteTableRow(1) should delete the second row when a cell in the second row is selected");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell in the second row is selected');
checkInputEvent(inputEvents[0], "when a cell in the second row is selected");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
getTableEditor().deleteTableRow(2);
is(editor.innerHTML, "",
"nsITableEditor.deleteTableRow(2) should delete the <table> since there is only 2 rows");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell in first row is selected and argument is same as number of rows');
checkInputEvent(inputEvents[0], "when a cell in first row is selected and argument is same as number of rows");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
getTableEditor().deleteTableRow(3);
is(editor.innerHTML, "",
"nsITableEditor.deleteTableRow(3) should delete the <table> when argument is larger than actual number of rows");
is(inputEvents.length, 1,
'Only one "input" event should be fired when argument is larger than actual number of rows');
checkInputEvent(inputEvents[0], "when argument is larger than actual number of rows");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
getTableEditor().deleteTableRow(2);
is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
"nsITableEditor.deleteTableRow(2) should delete the second row containing selected cell and next row");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell in second row and argument is same as the remaining rows');
checkInputEvent(inputEvents[0], "when a cell in second row and argument is same as the remaining rows");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
getTableEditor().deleteTableRow(3);
is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
"nsITableEditor.deleteTableRow(3) should delete the second row (containing selected cell) and the third row even though the argument is larger than the rows");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell in second row and argument is larger than the remaining rows');
checkInputEvent(inputEvents[0], "when a cell in second row and argument is larger than the remaining rows");
// Similar to selected a cell, when selection is in a cell, the cell should
// treated as selected.
@ -103,67 +148,91 @@ SimpleTest.waitForFocus(function() {
editor.innerHTML =
'<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select").firstChild);
selection.addRange(range);
getTableEditor().deleteTableRow(1);
is(editor.innerHTML, "<table><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>",
"nsITableEditor.deleteTableRow(1) should delete the first row when a cell in the first row contains selection range");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell in the first row contains selection range');
checkInputEvent(inputEvents[0], "when a cell in the first row contains selection range");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr></table>';
editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select").firstChild);
selection.addRange(range);
getTableEditor().deleteTableRow(1);
is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
"nsITableEditor.deleteTableRow(1) should delete the second row when a cell in the second row contains selection range");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell in the second row contains selection range');
checkInputEvent(inputEvents[0], "when a cell in the second row contains selection range");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select").firstChild);
selection.addRange(range);
getTableEditor().deleteTableRow(2);
is(editor.innerHTML, "",
"nsITableEditor.deleteTableRow(2) should delete the <table> since there is only 2 rows");
is(inputEvents.length, 1,
'Only one "input" event should be fired when all text in a cell in first row is selected and argument includes next row');
checkInputEvent(inputEvents[0], "when all text in a cell in first row is selected and argument includes next row");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select").firstChild);
selection.addRange(range);
getTableEditor().deleteTableRow(3);
is(editor.innerHTML, "",
"nsITableEditor.deleteTableRow(3) should delete the <table> when argument is larger than actual number of rows");
is(inputEvents.length, 1,
'Only one "input" event should be fired when all text in a cell in first row is selected and argument is same as number of all rows');
checkInputEvent(inputEvents[0], "when all text in a cell in first row is selected and argument is same as number of all rows");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select").firstChild);
selection.addRange(range);
getTableEditor().deleteTableRow(2);
is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
"nsITableEditor.deleteTableRow(2) should delete the second row containing a cell containing selection range and next row");
is(inputEvents.length, 1,
'Only one "input" event should be fired when all text in a cell is selected and argument is same than renaming number of rows');
checkInputEvent(inputEvents[0], "when all text in a cell is selected and argument is same than renaming number of rows");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select").firstChild);
selection.addRange(range);
getTableEditor().deleteTableRow(3);
is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>",
"nsITableEditor.deleteTableRow(3) should delete the second row (containing selection range) and the third row even though the argument is larger than the rows");
is(inputEvents.length, 1,
'Only one "input" event should be fired when all text in a cell in the second row and argument is larger than renaming number of rows');
checkInputEvent(inputEvents[0], "when all text in a cell in the second row and argument is larger than renaming number of rows");
// The argument should be ignored when 2 or more cells are selected.
// XXX If the argument is less than number of rows and cells in all rows are
@ -172,6 +241,7 @@ SimpleTest.waitForFocus(function() {
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr></table>';
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -181,10 +251,14 @@ SimpleTest.waitForFocus(function() {
getTableEditor().deleteTableRow(1);
is(editor.innerHTML, "<table><tbody></tbody></table>",
"nsITableEditor.deleteTableRow(1) should delete all rows if every row's cell is selected");
is(inputEvents.length, 1,
'Only one "input" event should be fired when cells in every row are selected #1');
checkInputEvent(inputEvents[0], "when cells in every row are selected #1");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr></table>';
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -194,10 +268,14 @@ SimpleTest.waitForFocus(function() {
getTableEditor().deleteTableRow(2);
is(editor.innerHTML, "",
"nsITableEditor.deleteTableRow(2) should delete the <table> since 2 is number of rows of the <table>");
is(inputEvents.length, 1,
'Only one "input" event should be fired when cells in every row are selected #2');
checkInputEvent(inputEvents[0], "when cells in every row are selected #2");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -207,10 +285,14 @@ SimpleTest.waitForFocus(function() {
getTableEditor().deleteTableRow(2);
is(editor.innerHTML, "",
"nsITableEditor.deleteTableRow(2) should delete the <table> since 2 is number of rows of the <table>");
is(inputEvents.length, 1,
'Only one "input" event should be fired when 2 cells in same row are selected');
checkInputEvent(inputEvents[0], "when 2 cells in same row are selected");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -220,10 +302,14 @@ SimpleTest.waitForFocus(function() {
getTableEditor().deleteTableRow(1);
is(editor.innerHTML, "<table><tbody><tr><td>cell3-1</td><td>cell3-2</td></tr></tbody></table>",
"nsITableEditor.deleteTableRow(1) should delete first 2 rows because cells in the both rows are selected");
is(inputEvents.length, 1,
'Only one "input" event should be fired when 2 cell elements in different rows are selected #1');
checkInputEvent(inputEvents[0], "when 2 cell elements in different rows are selected #1");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr><tr><td id="select2">cell3-1</td><td>cell3-2</td></tr></table>';
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
@ -233,48 +319,69 @@ SimpleTest.waitForFocus(function() {
getTableEditor().deleteTableRow(1);
is(editor.innerHTML, "<table><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>",
"nsITableEditor.deleteTableRow(1) should delete the first and the last rows because cells in the both rows are selected");
is(inputEvents.length, 1,
'Only one "input" event should be fired when 2 cell elements in different rows are selected #2');
checkInputEvent(inputEvents[0], "when 2 cell elements in different rows are selected #2");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td id="select" rowspan="2">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
getTableEditor().deleteTableRow(1);
is(editor.innerHTML, '<table><tbody><tr><td valign="top"><br></td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></tbody></table>',
"nsITableEditor.deleteTableRow(1) with a selected cell is rowspan=\"2\" should delete the first row and add empty cell to the second row");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell is selected and its rowspan is 2');
checkInputEvent(inputEvents[0], "when a cell is selected and its rowspan is 2");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td id="select" rowspan="3">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-2</td></tr><tr><td>cell3-2</td></tr></table>';
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
getTableEditor().deleteTableRow(1);
is(editor.innerHTML, '<table><tbody><tr><td rowspan="2" valign="top"><br></td><td>cell2-2</td></tr><tr><td>cell3-2</td></tr></tbody></table>',
"nsITableEditor.deleteTableRow(1) with a selected cell is rowspan=\"3\" should delete the first row and add empty cell whose rowspan is 2 to the second row");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell is selected and its rowspan is 3');
checkInputEvent(inputEvents[0], "when a cell is selected and its rowspan is 3");
// XXX Must be buggy case. When removing a row which does not have a cell due
// to rowspan, the rowspan is not changed properly.
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td rowspan="3">cell1-1</td><td>cell1-2</td></tr><tr><td id="select1">cell2-2</td></tr><tr><td>cell3-2</td></tr></table>';
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select1"));
selection.addRange(range);
getTableEditor().deleteTableRow(1);
is(editor.innerHTML, '<table><tbody><tr><td rowspan="1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell3-2</td></tr></tbody></table>',
"nsITableEditor.deleteTableRow(1) with selected cell in the second row should delete the second row and the row span should be adjusted");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell in 2nd row which is only cell defined by the row #1');
checkInputEvent(inputEvents[0], "when a cell in 2nd row which is only cell defined by the row #1");
selection.removeAllRanges();
editor.innerHTML =
'<table><tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
inputEvents = [];
range = document.createRange();
range.selectNode(document.getElementById("select"));
selection.addRange(range);
getTableEditor().deleteTableRow(1);
is(editor.innerHTML, '<table><tbody><tr><td rowspan="1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></tbody></table>',
"nsITableEditor.deleteTableRow(1) with selected cell in the second row should delete the second row and the row span should be adjusted");
is(inputEvents.length, 1,
'Only one "input" event should be fired when a cell in 2nd row which is only cell defined by the row #2');
checkInputEvent(inputEvents[0], "when a cell in 2nd row which is only cell defined by the row #2");
editor.removeEventListener("input", onInput);
SimpleTest.finish();
});

View File

@ -19,26 +19,54 @@ SimpleTest.waitForFocus(function() {
let editor = document.getElementById("content");
let selection = document.getSelection();
function checkInputEvent(aEvent, aDescription) {
ok(aEvent instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface ${aDescription}`);
is(aEvent.cancelable, false,
`"input" event should be never cancelable ${aDescription}`);
is(aEvent.bubbles, true,
`"input" event should always bubble ${aDescription}`);
}
let inputEvents = [];
function onInput(aEvent) {
inputEvents.push(aEvent);
}
editor.addEventListener("input", onInput);
inputEvents = [];
selection.collapse(editor.firstChild, 0);
getTableEditor().insertTableCell(1, false);
is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
"nsITableEditor.insertTableCell(1, false) should do nothing if selection is not in <table>");
is(inputEvents.length, 0,
'No "input" event should be fired when a call of nsITableEditor.insertTableCell(1, false) does nothing');
inputEvents = [];
getTableEditor().insertTableCell(1, true);
is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
"nsITableEditor.insertTableCell(1, true) should do nothing if selection is not in <table>");
is(inputEvents.length, 0,
'No "input" event should be fired when a call of nsITableEditor.insertTableCell(1, true) does nothing');
selection.removeAllRanges();
try {
inputEvents = [];
getTableEditor().insertTableCell(1, false);
ok(false, "getTableEditor().insertTableCell(1, false) without selection ranges should throw exception");
} catch (e) {
ok(true, "getTableEditor().insertTableCell(1, false) without selection ranges should throw exception");
is(inputEvents.length, 0,
'No "input" event should be fired when nsITableEditor.insertTableCell(1, false) causes exception due to no selection range');
}
try {
inputEvents = [];
getTableEditor().insertTableCell(1, true);
ok(false, "getTableEditor().insertTableCell(1, true) without selection ranges should throw exception");
} catch (e) {
ok(true, "getTableEditor().insertTableCell(1, true) without selection ranges should throw exception");
is(inputEvents.length, 0,
'No "input" event should be fired when nsITableEditor.insertTableCell(1, true) causes exception due to no selection range');
}
selection.removeAllRanges();
@ -49,6 +77,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().insertTableCell(1, false);
@ -58,6 +87,11 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.insertTableCell(1, false) should insert a cell before the cell containing selection");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection collapsed in a cell in middle row (before)');
if (inputEvents.length > 0) {
checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row (before)");
}
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -67,6 +101,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().insertTableCell(1, true);
@ -76,6 +111,11 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.insertTableCell(1, true) should insert a cell after the cell containing selection");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection collapsed in a cell in middle row (after)');
if (inputEvents.length > 0) {
checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row (after)");
}
// with rowspan.
@ -88,6 +128,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().insertTableCell(1, false);
@ -97,6 +138,11 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.insertTableCell(1, false) should insert a cell before the cell containing selection and moves the cell to right of the row-spanning cell element");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection collapsed in a cell in middle row and it has row-spanned cell (before)');
if (inputEvents.length > 0) {
checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row and it has row-spanned cell (before)");
}
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -106,6 +152,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().insertTableCell(1, true);
@ -115,6 +162,11 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td></tr>" +
"</tbody></table>",
"nsITableEditor.insertTableCell(1, true) should insert a cell after the cell containing selection and moves the cell to right of the row-spanning cell element");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection collapsed in a cell in middle row and it has row-spanned cell (after)');
if (inputEvents.length > 0) {
checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row and it has row-spanned cell (after)");
}
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -124,6 +176,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 1);
getTableEditor().insertTableCell(2, false);
@ -133,6 +186,11 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.insertTableCell(2, false) should insert 2 cells before the row-spanning cell containing selection");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection collapsed in a cell in row-spanning (before)');
if (inputEvents.length > 0) {
checkInputEvent(inputEvents[0], "when selection collapsed in a cell in row-spanning (before)");
}
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -142,6 +200,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 1);
getTableEditor().insertTableCell(2, true);
@ -151,6 +210,11 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.insertTableCell(2, false) should insert 2 cells after the row-spanning cell containing selection");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection collapsed in a cell in row-spanning (after)');
if (inputEvents.length > 0) {
checkInputEvent(inputEvents[0], "when selection collapsed in a cell in row-spanning (after)");
}
// with colspan
@ -161,6 +225,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().insertTableCell(1, false);
@ -169,6 +234,11 @@ SimpleTest.waitForFocus(function() {
'<tr><td colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
"</tbody></table>",
"nsITableEditor.insertTableCell(1, false) should insert a cell before the cell containing selection but do not modify col-spanning cell");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection collapsed in a cell whose next row cell is col-spanned (before)');
if (inputEvents.length > 0) {
checkInputEvent(inputEvents[0], "when selection collapsed in a cell whose next row cell is col-spanned (before)");
}
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -177,6 +247,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().insertTableCell(1, true);
@ -185,6 +256,11 @@ SimpleTest.waitForFocus(function() {
'<tr><td colspan="3">cell2-1</td></tr>' +
"</tbody></table>",
"nsITableEditor.insertTableCell(1, true) should insert a cell after the cell containing selection but do not modify col-spanning cell");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection collapsed in a cell whose next row cell is col-spanned (after)');
if (inputEvents.length > 0) {
checkInputEvent(inputEvents[0], "when selection collapsed in a cell whose next row cell is col-spanned (after)");
}
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -193,6 +269,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 1);
getTableEditor().insertTableCell(2, false);
@ -201,6 +278,11 @@ SimpleTest.waitForFocus(function() {
'<tr><td valign="top"><br></td><td valign="top"><br></td><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
"</tbody></table>",
"nsITableEditor.insertTableCell(2, false) should insert 2 cells before the col-spanning cell containing selection");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection collapsed in a cell which is col-spanning (before)');
if (inputEvents.length > 0) {
checkInputEvent(inputEvents[0], "when selection collapsed in a cell which is col-spanning (before)");
}
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -209,6 +291,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 1);
getTableEditor().insertTableCell(2, true);
@ -217,6 +300,13 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select" colspan="2">cell2-1</td><td valign="top"><br></td><td valign="top"><br></td><td>cell2-3</td></tr>' +
"</tbody></table>",
"nsITableEditor.insertTableCell(2, false) should insert 2 cells after the col-spanning cell containing selection");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection collapsed in a cell which is col-spanning (after)');
if (inputEvents.length > 0) {
checkInputEvent(inputEvents[0], "when selection collapsed in a cell which is col-spanning (after)");
}
editor.removeEventListener("input", onInput);
SimpleTest.finish();
});

View File

@ -19,26 +19,54 @@ SimpleTest.waitForFocus(function() {
let editor = document.getElementById("content");
let selection = document.getSelection();
function checkInputEvent(aEvent, aDescription) {
ok(aEvent instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface ${aDescription}`);
is(aEvent.cancelable, false,
`"input" event should be never cancelable ${aDescription}`);
is(aEvent.bubbles, true,
`"input" event should always bubble ${aDescription}`);
}
let inputEvents = [];
function onInput(aEvent) {
inputEvents.push(aEvent);
}
editor.addEventListener("input", onInput);
inputEvents = [];
selection.collapse(editor.firstChild, 0);
getTableEditor().insertTableColumn(1, false);
is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
"nsITableEditor.insertTableColumn(1, false) should do nothing if selection is not in <table>");
is(inputEvents.length, 0,
'No "input" event should be fired when a call of nsITableEditor.insertTableColumn(1, false) does nothing');
inputEvents = [];
getTableEditor().insertTableColumn(1, true);
is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
"nsITableEditor.insertTableColumn(1, true) should do nothing if selection is not in <table>");
is(inputEvents.length, 0,
'No "input" event should be fired when a call of nsITableEditor.insertTableColumn(1, true) does nothing');
selection.removeAllRanges();
try {
inputEvents = [];
getTableEditor().insertTableColumn(1, false);
ok(false, "getTableEditor().insertTableColumn(1, false) without selection ranges should throw exception");
} catch (e) {
ok(true, "getTableEditor().insertTableColumn(1, false) without selection ranges should throw exception");
is(inputEvents.length, 0,
'No "input" event should be fired when nsITableEditor.insertTableColumn(1, false) causes exception due to no selection range');
}
try {
inputEvents = [];
getTableEditor().insertTableColumn(1, true);
ok(false, "getTableEditor().insertTableColumn(1, true) without selection ranges should throw exception");
} catch (e) {
ok(true, "getTableEditor().insertTableColumn(1, true) without selection ranges should throw exception");
is(inputEvents.length, 0,
'No "input" event should be fired when nsITableEditor.insertTableColumn(1, true) causes exception due to no selection range');
}
selection.removeAllRanges();
@ -48,6 +76,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().insertTableColumn(1, false);
@ -56,6 +85,9 @@ SimpleTest.waitForFocus(function() {
'<tr><td>cell2-1</td><td valign="top"><br></td><td>cell2-2</td><td>cell2-3</td></tr>' +
"</tbody></table>",
"nsITableEditor.insertTableColumn(1, false) should insert a column to left of the second column");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection is collapsed in a cell in second column (before)');
checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column (before)");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -64,6 +96,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().insertTableColumn(1, true);
@ -72,6 +105,9 @@ SimpleTest.waitForFocus(function() {
'<tr><td>cell2-1</td><td>cell2-2</td><td valign="top"><br></td><td>cell2-3</td></tr>' +
"</tbody></table>",
"nsITableEditor.insertTableColumn(1, false) should insert a column to right of the second column");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection is collapsed in a cell in second column (after)');
checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column (after)");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -80,6 +116,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().insertTableColumn(1, false);
@ -88,6 +125,9 @@ SimpleTest.waitForFocus(function() {
'<tr><td colspan="3">cell2-1</td><td>cell2-3</td></tr>' +
"</tbody></table>",
"nsITableEditor.insertTableColumn(1, false) should insert a column to left of the second column and colspan in the first column should be increased");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection is collapsed in a cell in second column whose next row cell is col-spanned (before)');
checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column whose next row cell is col-spanned (before)");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -96,6 +136,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().insertTableColumn(1, true);
@ -104,6 +145,9 @@ SimpleTest.waitForFocus(function() {
'<tr><td colspan="4">cell2-1</td></tr>' +
"</tbody></table>",
"nsITableEditor.insertTableColumn(1, true) should insert a column to right of the second column and colspan in the first column should be increased");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection is collapsed in a cell in second column whose next row cell is col-spanned (after)');
checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column whose next row cell is col-spanned (after)");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -112,6 +156,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 1);
getTableEditor().insertTableColumn(2, false);
@ -120,6 +165,9 @@ SimpleTest.waitForFocus(function() {
'<tr><td valign="top"><br></td><td valign="top"><br></td><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' +
"</tbody></table>",
"nsITableEditor.insertTableColumn(2, false) should insert 2 columns to left of the first column");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection is collapsed in a cell which is col-spanning (before)');
checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is col-spanning (before)");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -128,6 +176,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 1);
getTableEditor().insertTableColumn(2, true);
@ -136,6 +185,11 @@ SimpleTest.waitForFocus(function() {
'<tr><td id="select" colspan="2">cell2-1</td><td valign="top"><br></td><td valign="top"><br></td><td>cell2-3</td></tr>' +
"</tbody></table>",
"nsITableEditor.insertTableColumn(2, false) should insert 2 columns to right of the second column (i.e., right of the right-most column of the column-spanning cell");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection is collapsed in a cell which is col-spanning (after)');
checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is col-spanning (after)");
editor.removeEventListener("input", onInput);
SimpleTest.finish();
});

View File

@ -19,26 +19,54 @@ SimpleTest.waitForFocus(function() {
let editor = document.getElementById("content");
let selection = document.getSelection();
function checkInputEvent(aEvent, aDescription) {
ok(aEvent instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface ${aDescription}`);
is(aEvent.cancelable, false,
`"input" event should be never cancelable ${aDescription}`);
is(aEvent.bubbles, true,
`"input" event should always bubble ${aDescription}`);
}
let inputEvents = [];
function onInput(aEvent) {
inputEvents.push(aEvent);
}
editor.addEventListener("input", onInput);
inputEvents = [];
selection.collapse(editor.firstChild, 0);
getTableEditor().insertTableRow(1, false);
is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
"nsITableEditor.insertTableRow(1, false) should do nothing if selection is not in <table>");
is(inputEvents.length, 0,
'No "input" event should be fired when a call of nsITableEditor.insertTableRow(1, false) does nothing');
inputEvents = [];
getTableEditor().insertTableRow(1, true);
is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
"nsITableEditor.insertTableRow(1, true) should do nothing if selection is not in <table>");
is(inputEvents.length, 0,
'No "input" event should be fired when a call of nsITableEditor.insertTableRow(1, true) does nothing');
selection.removeAllRanges();
try {
inputEvents = [];
getTableEditor().insertTableRow(1, false);
ok(false, "getTableEditor().insertTableRow(1, false) without selection ranges should throw exception");
} catch (e) {
ok(true, "getTableEditor().insertTableRow(1, false) without selection ranges should throw exception");
is(inputEvents.length, 0,
'No "input" event should be fired when nsITableEditor.insertTableRow(1, false) causes exception due to no selection range');
}
try {
inputEvents = [];
getTableEditor().insertTableRow(1, true);
ok(false, "getTableEditor().insertTableRow(1, true) without selection ranges should throw exception");
} catch (e) {
ok(true, "getTableEditor().insertTableRow(1, true) without selection ranges should throw exception");
is(inputEvents.length, 0,
'No "input" event should be fired when nsITableEditor.insertTableRow(1, true) causes exception due to no selection range');
}
selection.removeAllRanges();
@ -49,6 +77,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().insertTableRow(1, false);
@ -59,6 +88,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.insertTableRow(1, false) should insert a row above the second row");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection is collapsed in a cell in second row (before)');
checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row (before)");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -68,6 +100,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().insertTableRow(1, true);
@ -78,6 +111,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.insertTableRow(1, true) should insert a row below the second row");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection is collapsed in a cell in second row (after)');
checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row (after)");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -87,6 +123,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().insertTableRow(1, false);
@ -97,6 +134,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.insertTableRow(1, false) should insert a row above the second row and rowspan in the first row should be increased");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection is collapsed in a cell in second row which has row-spanned cell (before)');
checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row which has row-spanned cell (before)");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -106,6 +146,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 0);
getTableEditor().insertTableRow(1, true);
@ -116,6 +157,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td></tr>" +
"</tbody></table>",
"nsITableEditor.insertTableRow(1, true) should insert a row below the second row and rowspan in the first row should be increased");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection is collapsed in a cell in second row which has row-spanned cell (after)');
checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row which has row-spanned cell (after)");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -125,6 +169,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 1);
getTableEditor().insertTableRow(2, false);
@ -136,6 +181,9 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.insertTableRow(2, false) should insert 2 rows above the first row");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection is collapsed in a cell which is row-spanning (before)');
checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is row-spanning (before)");
selection.removeAllRanges();
editor.innerHTML = "<table>" +
@ -145,6 +193,7 @@ SimpleTest.waitForFocus(function() {
"</table>";
editor.focus();
editor.scrollTop; // layout information required.
inputEvents = [];
selection.setBaseAndExtent(document.getElementById("select").firstChild, 0,
document.getElementById("select").firstChild, 1);
getTableEditor().insertTableRow(2, true);
@ -156,6 +205,11 @@ SimpleTest.waitForFocus(function() {
"<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
"</tbody></table>",
"nsITableEditor.insertTableRow(2, false) should insert 2 rows below the second row (i.e., below the bottom row of the row-spanning cell");
is(inputEvents.length, 1,
'Only one "input" event should be fired when selection is collapsed in a cell which is row-spanning (after)');
checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is row-spanning (after)");
editor.removeEventListener("input", onInput);
SimpleTest.finish();
});

View File

@ -85,12 +85,31 @@ SimpleTest.waitForFocus(async function() {
let basePosX = rect.width * baseX;
let basePosY = rect.height * baseY;
let inputEventExpected = true;
function onInput(aEvent) {
if (!inputEventExpected) {
ok(false, "\"input\" event shouldn't be fired after stopping resizing");
return;
}
ok(aEvent instanceof InputEvent,
'"input" event should be dispatched with InputEvent interface');
is(aEvent.cancelable, false,
'"input" event should be never cancelable');
is(aEvent.bubbles, true,
'"input" event should always bubble');
}
content.addEventListener("input", onInput);
// Click on the correct resizer
synthesizeMouse(target, basePosX, basePosY, {type: "mousedown"});
// Drag it delta pixels to the right and bottom (or maybe left and top!)
synthesizeMouse(target, basePosX + deltaX, basePosY + deltaY, {type: "mousemove"});
// Release the mouse button
synthesizeMouse(target, basePosX + deltaX, basePosY + deltaY, {type: "mouseup"});
inputEventExpected = false;
// Move the mouse delta more pixels to the same direction to make sure that the
// resize operation has stopped.
synthesizeMouse(target, basePosX + deltaX * 2, basePosY + deltaY * 2, {type: "mousemove"});
@ -104,6 +123,8 @@ SimpleTest.waitForFocus(async function() {
let newRect = target.getBoundingClientRect();
isfuzzy(newRect.width, rect.width + expectedDeltaX, 2, description + "The width should be increased by " + expectedDeltaX + " pixels");
isfuzzy(newRect.height, rect.height + expectedDeltaY, 2, description + "The height should be increased by " + expectedDeltaY + "pixels");
content.removeEventListener("input", onInput);
}
// Account for changes in the resizing behavior when we're trying to preserve

View File

@ -21,24 +21,56 @@ SimpleTest.waitForFocus(() => {
textarea.focus();
function checkInputEvent(aEvent, aDescription) {
ok(aEvent instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface ${aDescription}`);
is(aEvent.cancelable, false,
`"input" event should be never cancelable ${aDescription}`);
is(aEvent.bubbles, true,
`"input" event should always bubble ${aDescription}`);
}
let inputEvents = [];
function onInput(aEvent) {
inputEvents.push(aEvent);
}
SpecialPowers.Cu.import(
"resource://testing-common/AsyncSpellCheckTestHelper.jsm")
.onSpellCheck(textarea, () => {
SimpleTest.executeSoon(() => {
textarea.addEventListener("input", onInput);
let misspelledWord = inlineSpellChecker.getMisspelledWord(editor.rootElement.firstChild, 5);
is(misspelledWord.startOffset, 4,
"Misspelled word should start from 4");
is(misspelledWord.endOffset, 7,
"Misspelled word should end at 7");
inputEvents = [];
inlineSpellChecker.replaceWord(editor.rootElement.firstChild, 5, "aux");
is(textarea.value, "abc aux abc",
"'abx' should be replaced with 'aux'");
is(inputEvents.length, 1,
'Only one "input" event should be fired when replacing a word with spellchecker');
checkInputEvent(inputEvents[0], "when replacing a word with spellchecker");
inputEvents = [];
synthesizeKey("z", { accelKey: true });
is(textarea.value, "abc abx abc",
"'abx' should be restored by undo");
is(inputEvents.length, 1,
'Only one "input" event should be fired when undoing the replacing word');
checkInputEvent(inputEvents[0], "when undoing the replacing word");
inputEvents = [];
synthesizeKey("z", { accelKey: true, shiftKey: true });
is(textarea.value, "abc aux abc",
"'aux' should be restored by redo");
is(inputEvents.length, 1,
'Only one "input" event should be fired when redoing the replacing word');
checkInputEvent(inputEvents[0], "when redoing the replacing word");
textarea.removeEventListener("input", onInput);
SimpleTest.finish();
});

View File

@ -29,11 +29,46 @@ SimpleTest.waitForFocus(function() {
document.getElementById("textarea"),
];
for (let editableElement of editableElements) {
function checkInputEvent(aEvent, aDescription) {
ok(aEvent instanceof InputEvent,
`"input" event should be dispatched with InputEvent interface ${aDescription}`);
is(aEvent.cancelable, false,
`"input" event should be never cancelable ${aDescription}`);
is(aEvent.bubbles, true,
`"input" event should always bubble ${aDescription}`);
}
let inputEvents = [];
function onInput(aEvent) {
inputEvents.push(aEvent);
}
editableElement.addEventListener("input", onInput);
editableElement.focus();
inputEvents = [];
synthesizeKey("a");
is(inputEvents.length, 1,
`Only one "input" event should be fired when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`);
checkInputEvent(inputEvents[0], `when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`);
inputEvents = [];
synthesizeKey("c");
is(inputEvents.length, 1,
`Only one "input" event should be fired when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`);
checkInputEvent(inputEvents[0], `when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`);
inputEvents = [];
synthesizeKey("KEY_ArrowLeft");
is(inputEvents.length, 0,
`No "input" event should be fired when pressing "ArrowLeft" key on <${editableElement.tagName.toLowerCase()}> element`);
inputEvents = [];
synthesizeKey("b");
is(inputEvents.length, 1,
`Only one "input" event should be fired when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`);
checkInputEvent(inputEvents[0], `when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`);
let editor = SpecialPowers.wrap(editableElement).editor;
let transactionManager = editor.transactionManager;
is(transactionManager.numberOfUndoItems, 2,
@ -41,15 +76,30 @@ SimpleTest.waitForFocus(function() {
// Defined as nsITextControlElement::DEFAULT_UNDO_CAP
is(transactionManager.maxTransactionCount, 1000,
editableElement.tagName + ": Initially, transaction manager should be able to have 1,000 undo items");
inputEvents = [];
editableElement.value = "def";
is(inputEvents.length, 0,
`No "input" event should be fired when setting value of <${editableElement.tagName.toLowerCase()}> element`);
is(transactionManager.numberOfUndoItems, 0,
editableElement.tagName + ": After setting value, all undo items must be deleted");
is(transactionManager.maxTransactionCount, 1000,
editableElement.tagName + ": After setting value, maximum transaction count should be restored to the previous value");
inputEvents = [];
synthesizeKey("a");
is(inputEvents.length, 1,
`Only one "input" event should be fired when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`);
checkInputEvent(inputEvents[0], `when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`);
inputEvents = [];
synthesizeKey("z", { accelKey: true });
is(editableElement.value, "def",
editableElement.tagName + ": undo should work after setting value");
is(inputEvents.length, 1,
`Only one "input" event should be fired when undoing on <${editableElement.tagName.toLowerCase()}> element`);
checkInputEvent(inputEvents[0], `when undoing on <${editableElement.tagName.toLowerCase()}> element`);
// Disable undo/redo.
editor.enableUndo(0);
@ -58,6 +108,8 @@ SimpleTest.waitForFocus(function() {
editableElement.value = "hij";
is(transactionManager.maxTransactionCount, 0,
editableElement.tagName + ": Transaction manager should not be able to have undo items after setting value");
editableElement.removeEventListener("input", onInput);
}
SimpleTest.finish();
});

View File

@ -191,8 +191,12 @@ impl SpatialNode {
true
}
pub fn mark_uninvertible(&mut self) {
pub fn mark_uninvertible(
&mut self,
state: &TransformUpdateState,
) {
self.invertible = false;
self.coordinate_system_id = state.current_coordinate_system_id;
self.world_content_transform = LayoutToWorldFastTransform::identity();
self.world_viewport_transform = LayoutToWorldFastTransform::identity();
}
@ -221,7 +225,7 @@ impl SpatialNode {
// If any of our parents was not rendered, we are not rendered either and can just
// quit here.
if !state.invertible {
self.mark_uninvertible();
self.mark_uninvertible(state);
return;
}
@ -233,7 +237,7 @@ impl SpatialNode {
// translations which should be invertible.
match self.node_type {
SpatialNodeType::ReferenceFrame(info) if !info.invertible => {
self.mark_uninvertible();
self.mark_uninvertible(state);
return;
}
_ => self.invertible = true,

View File

@ -1 +1 @@
6ffc7cfe02f2a914a7d4338510277988643cc441
229436b578701fc74a009d6cedc6b2a3ae313f77

View File

@ -128,6 +128,7 @@ public:
* @param aRepaint if true then force repaint (NOTE: we always force repaint currently)
* @note This method might destroy |this|.
*/
MOZ_CAN_RUN_SCRIPT_BOUNDARY
virtual void SetFocus(bool aOn, bool aRepaint) override;
bool IsDroppedDown() { return mDroppedDown; }

View File

@ -459,11 +459,11 @@ nsFileControlFrame::DnDListener::HandleEvent(Event* aEvent)
inputElement->SetFiles(fileList, true);
}
nsContentUtils::DispatchTrustedEvent(inputElement->OwnerDoc(),
static_cast<nsINode*>(inputElement),
NS_LITERAL_STRING("input"),
CanBubble::eYes,
Cancelable::eNo);
RefPtr<TextEditor> textEditor;
DebugOnly<nsresult> rvIgnored =
nsContentUtils::DispatchInputEvent(inputElement);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch input event");
nsContentUtils::DispatchTrustedEvent(inputElement->OwnerDoc(),
static_cast<nsINode*>(inputElement),
NS_LITERAL_STRING("change"),

View File

@ -123,7 +123,9 @@ protected:
: MouseListener(aFrame)
{}
NS_DECL_NSIDOMEVENTLISTENER
// nsIDOMEventListener
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD HandleEvent(mozilla::dom::Event* aEvent) override;
nsresult GetBlobImplForWebkitDirectory(mozilla::dom::FileList* aFileList,
mozilla::dom::BlobImpl** aBlobImpl);

View File

@ -70,7 +70,10 @@ public:
void SetFrame(nsListControlFrame *aFrame) { mFrame = aFrame; }
NS_DECL_ISUPPORTS
NS_DECL_NSIDOMEVENTLISTENER
// nsIDOMEventListener
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD HandleEvent(Event* aEvent) override;
private:
~nsListEventListener() {}
@ -1390,14 +1393,17 @@ nsListControlFrame::FireOnInputAndOnChange()
}
}
nsCOMPtr<nsIContent> content = mContent;
RefPtr<Element> element = Element::FromNodeOrNull(mContent);
if (NS_WARN_IF(!element)) {
return;
}
// Dispatch the input event.
nsContentUtils::DispatchTrustedEvent(content->OwnerDoc(), content,
NS_LITERAL_STRING("input"),
CanBubble::eYes,
Cancelable::eNo);
DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(element);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to dispatch input event");
// Dispatch the change event.
nsContentUtils::DispatchTrustedEvent(content->OwnerDoc(), content,
nsContentUtils::DispatchTrustedEvent(element->OwnerDoc(), element,
NS_LITERAL_STRING("change"),
CanBubble::eYes,
Cancelable::eNo);

View File

@ -134,6 +134,7 @@ public:
* Dispatch a DOM oninput and onchange event synchroniously.
* @note This method might destroy the frame, pres shell and other objects.
*/
MOZ_CAN_RUN_SCRIPT
void FireOnInputAndOnChange();
/**
@ -160,10 +161,13 @@ public:
* @note These methods might destroy the frame, pres shell and other objects.
*/
nsresult MouseDown(mozilla::dom::Event* aMouseEvent);
MOZ_CAN_RUN_SCRIPT
nsresult MouseUp(mozilla::dom::Event* aMouseEvent);
nsresult MouseMove(mozilla::dom::Event* aMouseEvent);
nsresult DragMove(mozilla::dom::Event* aMouseEvent);
MOZ_CAN_RUN_SCRIPT
nsresult KeyDown(mozilla::dom::Event* aKeyEvent);
MOZ_CAN_RUN_SCRIPT
nsresult KeyPress(mozilla::dom::Event* aKeyEvent);
/**
@ -257,6 +261,7 @@ protected:
* @note This method might destroy the frame, pres shell and other objects.
* Returns false if calling it destroyed |this|.
*/
MOZ_CAN_RUN_SCRIPT
bool UpdateSelection();
/**
@ -271,6 +276,7 @@ protected:
* Toggles (show/hide) the combobox dropdown menu.
* @note This method might destroy the frame, pres shell and other objects.
*/
MOZ_CAN_RUN_SCRIPT
void DropDownToggleKey(mozilla::dom::Event* aKeyEvent);
/**
@ -378,6 +384,7 @@ protected:
bool HandleListSelection(mozilla::dom::Event * aDOMEvent,
int32_t selectedIndex);
void InitSelectionRange(int32_t aClickedIndex);
MOZ_CAN_RUN_SCRIPT
void PostHandleKeyEvent(int32_t aNewIndex, uint32_t aCharCode,
bool aIsShift, bool aIsControlOrMeta);

View File

@ -160,7 +160,8 @@ PromptFactory.prototype = {
},
_dispatchEvents: function(aElement) {
// Fire both "input" and "change" events for <select> and <input>.
// Fire both "input" and "change" events for <select> and <input> for
// date/time.
aElement.dispatchEvent(new aElement.ownerGlobal.Event("input", { bubbles: true }));
aElement.dispatchEvent(new aElement.ownerGlobal.Event("change", { bubbles: true }));
},

View File

@ -615,8 +615,13 @@ class AccessibilityTest : BaseSessionTest() {
arrayOf("document", "$('#iframe').contentDocument").map { doc ->
mainSession.evaluateJS("""new Promise(resolve =>
$doc.querySelector('${entry.key}').addEventListener(
'input', event => resolve([event.target.value, '${entry.value}']),
{ once: true }))""").asJSPromise()
'input', event => {
let eventInterface =
event instanceof InputEvent ? "InputEvent" :
event instanceof UIEvent ? "UIEvent" :
event instanceof Event ? "Event" : "Unknown";
resolve([event.target.value, '${entry.value}', eventInterface]);
}, { once: true }))""").asJSPromise()
}
}
@ -665,8 +670,9 @@ class AccessibilityTest : BaseSessionTest() {
autoFillChild(View.NO_ID, createNodeInfo(View.NO_ID))
// Wait on the promises and check for correct values.
for ((actual, expected) in promises.map { it.value.asJSList<String>() }) {
for ((actual, expected, eventInterface) in promises.map { it.value.asJSList<String>() }) {
assertThat("Auto-filled value must match", actual, equalTo(expected))
assertThat("input event should be dispatched with InputEvent interface", eventInterface, equalTo("InputEvent"))
}
}

View File

@ -269,8 +269,13 @@ class ContentDelegateTest : BaseSessionTest() {
arrayOf("document", "$('#iframe').contentDocument").map { doc ->
mainSession.evaluateJS("""new Promise(resolve =>
$doc.querySelector('${entry.key}').addEventListener(
'input', event => resolve([event.target.value, '${entry.value}']),
{ once: true }))""").asJSPromise()
'input', event => {
let eventInterface =
event instanceof InputEvent ? "InputEvent" :
event instanceof UIEvent ? "UIEvent" :
event instanceof Event ? "Event" : "Unknown";
resolve([event.target.value, '${entry.value}', eventInterface]);
}, { once: true }))""").asJSPromise()
}
}
@ -348,8 +353,9 @@ class ContentDelegateTest : BaseSessionTest() {
mainSession.textInput.autofill(autoFillValues)
// Wait on the promises and check for correct values.
for ((actual, expected) in promises.map { it.value.asJSList<String>() }) {
for ((actual, expected, eventInterface) in promises.map { it.value.asJSList<String>() }) {
assertThat("Auto-filled value must match", actual, equalTo(expected))
assertThat("input event should be dispatched with InputEvent interface", eventInterface, equalTo("InputEvent"))
}
}

View File

@ -127,14 +127,7 @@ class GeckoViewAutoFill {
if (element instanceof window.HTMLInputElement &&
!element.disabled && element.parentElement) {
element.value = value;
// Fire both "input" and "change" events.
element.dispatchEvent(new element.ownerGlobal.Event(
"input", { bubbles: true }));
element.dispatchEvent(new element.ownerGlobal.Event(
"change", { bubbles: true }));
element.setUserInput(value);
if (winUtils && element.value === value) {
// Add highlighting for autofilled fields.
winUtils.addManuallyManagedState(element, AUTOFILL_STATE);

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