/* * This test checks that focus is adjusted properly when switching tabs. */ var testPage1 = ""; var testPage2 = ""; var testPage3 = ""; const fm = Services.focus; function EventStore() { this["main-window"] = []; this.window1 = []; this.window2 = []; } EventStore.prototype = { push(event) { if (event.includes("browser1") || event.includes("browser2")) { this["main-window"].push(event); } else if (event.includes("1")) { this.window1.push(event); } else if (event.includes("2")) { this.window2.push(event); } else { this["main-window"].push(event); } }, }; var tab1 = null; var tab2 = null; var browser1 = null; var browser2 = null; var _lastfocus; var _lastfocuswindow = null; var actualEvents = new EventStore(); var expectedEvents = new EventStore(); var currentTestName = ""; var _expectedElement = null; var _expectedWindow = null; var currentPromiseResolver = null; function getFocusedElementForBrowser(browser, dontCheckExtraFocus = false) { return SpecialPowers.spawn( browser, [dontCheckExtraFocus], dontCheckExtraFocusChild => { let focusedWindow = {}; let node = Services.focus.getFocusedElementForWindow( content, false, focusedWindow ); let details = "Focus is " + (node ? node.id : ""); /* Check focus manager properties. Add an error onto the string if they are not what is expected which will cause matching to fail in the parent process. */ let doc = content.document; if (!dontCheckExtraFocusChild) { if (Services.focus.focusedElement != node) { details += ""; } if ( Services.focus.focusedWindow && Services.focus.focusedWindow != content ) { details += ""; } if ((Services.focus.focusedWindow == content) != doc.hasFocus()) { details += ""; } if ( (Services.focus.focusedElement && doc.activeElement != Services.focus.focusedElement) || (!Services.focus.focusedElement && doc.activeElement != doc.body) ) { details += ""; } } return details; } ); } function focusInChild(event) { function getWindowDocId(target) { return String(target.location).includes("1") ? "window1" : "window2"; } // Stop the shim code from seeing this event process. event.stopImmediatePropagation(); var id; if (event.target instanceof Ci.nsIDOMWindow) { id = getWindowDocId(event.originalTarget) + "-window"; } else if (event.target.nodeType == event.target.DOCUMENT_NODE) { id = getWindowDocId(event.originalTarget) + "-document"; } else { id = event.originalTarget.id; } let window = event.target.ownerGlobal; if (!window._eventsOccurred) { window._eventsOccurred = []; } window._eventsOccurred.push(event.type + ": " + id); return true; } function focusElementInChild(elementid, elementtype) { let browser = elementid.includes("1") ? browser1 : browser2; return SpecialPowers.spawn(browser, [elementid, elementtype], (id, type) => { content.document.getElementById(id)[type](); }); } add_task(async function () { tab1 = BrowserTestUtils.addTab(gBrowser); browser1 = gBrowser.getBrowserForTab(tab1); tab2 = BrowserTestUtils.addTab(gBrowser); browser2 = gBrowser.getBrowserForTab(tab2); await promiseTabLoadEvent(tab1, "data:text/html," + escape(testPage1)); await promiseTabLoadEvent(tab2, "data:text/html," + escape(testPage2)); gURLBar.focus(); await SimpleTest.promiseFocus(); // In these listeners, focusInChild is used to cache details about the event // on a temporary on the window (window._eventsOccurred), so that it can be // retrieved later within compareFocusResults. focusInChild always returns true. // compareFocusResults is called each time event occurs to check that the // right events happened. let listenersToRemove = []; listenersToRemove.push( BrowserTestUtils.addContentEventListener( browser1, "focus", compareFocusResults, { capture: true }, focusInChild ) ); listenersToRemove.push( BrowserTestUtils.addContentEventListener( browser1, "blur", compareFocusResults, { capture: true }, focusInChild ) ); listenersToRemove.push( BrowserTestUtils.addContentEventListener( browser2, "focus", compareFocusResults, { capture: true }, focusInChild ) ); listenersToRemove.push( BrowserTestUtils.addContentEventListener( browser2, "blur", compareFocusResults, { capture: true }, focusInChild ) ); // Get the content processes to do something, so that we can better // ensure that the listeners added above will have actually been added // in the tabs. await SpecialPowers.spawn(browser1, [], () => {}); await SpecialPowers.spawn(browser2, [], () => {}); _lastfocus = "urlbar"; _lastfocuswindow = "main-window"; window.addEventListener("focus", _browser_tabfocus_test_eventOccured, true); window.addEventListener("blur", _browser_tabfocus_test_eventOccured, true); // make sure that the focus initially starts out blank var focusedWindow = {}; let focused = await getFocusedElementForBrowser(browser1); is(focused, "Focus is ", "initial focus in tab 1"); focused = await getFocusedElementForBrowser(browser2); is(focused, "Focus is ", "initial focus in tab 2"); is( document.activeElement, gURLBar.inputField, "focus after loading two tabs" ); await expectFocusShiftAfterTabSwitch( tab2, "window2", null, true, "after tab change, focus in new tab" ); focused = await getFocusedElementForBrowser(browser2); is( focused, "Focus is ", "focusedElement after tab change, focus in new tab" ); // switching tabs when nothing in the new tab is focused // should focus the browser await expectFocusShiftAfterTabSwitch( tab1, "window1", null, true, "after tab change, focus in original tab" ); focused = await getFocusedElementForBrowser(browser1); is( focused, "Focus is ", "focusedElement after tab change, focus in original tab" ); // focusing a button in the current tab should focus it await expectFocusShift( () => focusElementInChild("button1", "focus"), "window1", "button1", true, "after button focused" ); focused = await getFocusedElementForBrowser(browser1); is( focused, "Focus is button1", "focusedElement in first browser after button focused" ); // focusing a button in a background tab should not change the actual // focus, but should set the focus that would be in that background tab to // that button. await expectFocusShift( () => focusElementInChild("button2", "focus"), "window1", "button1", false, "after button focus in unfocused tab" ); focused = await getFocusedElementForBrowser(browser1, false); is( focused, "Focus is button1", "focusedElement in first browser after button focus in unfocused tab" ); focused = await getFocusedElementForBrowser(browser2, true); is( focused, "Focus is button2", "focusedElement in second browser after button focus in unfocused tab" ); // switching tabs should now make the button in the other tab focused await expectFocusShiftAfterTabSwitch( tab2, "window2", "button2", true, "after tab change with button focused" ); // blurring an element in a background tab should not change the active // focus, but should clear the focus in that tab. await expectFocusShift( () => focusElementInChild("button1", "blur"), "window2", "button2", false, "focusedWindow after blur in unfocused tab" ); focused = await getFocusedElementForBrowser(browser1, true); is( focused, "Focus is ", "focusedElement in first browser after focus in unfocused tab" ); focused = await getFocusedElementForBrowser(browser2, false); is( focused, "Focus is button2", "focusedElement in second browser after focus in unfocused tab" ); // When focus is in the tab bar, it should be retained there await expectFocusShift( () => gBrowser.selectedTab.focus(), "main-window", "tab2", true, "focusing tab element" ); await expectFocusShiftAfterTabSwitch( tab1, "main-window", "tab1", true, "tab change when selected tab element was focused" ); let switchWaiter = new Promise((resolve, reject) => { gBrowser.addEventListener( "TabSwitchDone", function () { executeSoon(resolve); }, { once: true } ); }); await expectFocusShiftAfterTabSwitch( tab2, "main-window", "tab2", true, "another tab change when selected tab element was focused" ); // Wait for the paint on the second browser so that any post tab-switching // stuff has time to complete before blurring the tab. Otherwise, the // _adjustFocusAfterTabSwitch in tabbrowser gets confused and isn't sure // what tab is really focused. await switchWaiter; await expectFocusShift( () => gBrowser.selectedTab.blur(), "main-window", null, true, "blurring tab element" ); // focusing the url field should switch active focus away from the browser but // not clear what would be the focus in the browser await focusElementInChild("button1", "focus"); await expectFocusShift( () => gURLBar.focus(), "main-window", "urlbar", true, "focusedWindow after url field focused" ); focused = await getFocusedElementForBrowser(browser1, true); is( focused, "Focus is button1", "focusedElement after url field focused, first browser" ); focused = await getFocusedElementForBrowser(browser2, true); is( focused, "Focus is button2", "focusedElement after url field focused, second browser" ); await expectFocusShift( () => gURLBar.blur(), "main-window", null, true, "blurring url field" ); // when a chrome element is focused, switching tabs to a tab with a button // with the current focus should focus the button await expectFocusShiftAfterTabSwitch( tab1, "window1", "button1", true, "after tab change, focus in url field, button focused in new tab" ); focused = await getFocusedElementForBrowser(browser1, false); is( focused, "Focus is button1", "after switch tab, focus in unfocused tab, first browser" ); focused = await getFocusedElementForBrowser(browser2, true); is( focused, "Focus is button2", "after switch tab, focus in unfocused tab, second browser" ); // blurring an element in the current tab should clear the active focus await expectFocusShift( () => focusElementInChild("button1", "blur"), "window1", null, true, "after blur in focused tab" ); focused = await getFocusedElementForBrowser(browser1, false); is( focused, "Focus is ", "focusedWindow after blur in focused tab, child" ); focusedWindow = {}; is( fm.getFocusedElementForWindow(window, false, focusedWindow), browser1, "focusedElement after blur in focused tab, parent" ); // blurring an non-focused url field should have no effect await expectFocusShift( () => gURLBar.blur(), "window1", null, false, "after blur in unfocused url field" ); focusedWindow = {}; is( fm.getFocusedElementForWindow(window, false, focusedWindow), browser1, "focusedElement after blur in unfocused url field" ); // switch focus to a tab with a currently focused element await expectFocusShiftAfterTabSwitch( tab2, "window2", "button2", true, "after switch from unfocused to focused tab" ); focused = await getFocusedElementForBrowser(browser2, true); is( focused, "Focus is button2", "focusedElement after switch from unfocused to focused tab" ); // clearing focus on the chrome window should switch the focus to the // chrome window await expectFocusShift( () => fm.clearFocus(window), "main-window", null, true, "after switch to chrome with no focused element" ); focusedWindow = {}; is( fm.getFocusedElementForWindow(window, false, focusedWindow), null, "focusedElement after switch to chrome with no focused element" ); // switch focus to another tab when neither have an active focus await expectFocusShiftAfterTabSwitch( tab1, "window1", null, true, "focusedWindow after tab switch from no focus to no focus" ); focused = await getFocusedElementForBrowser(browser1, false); is( focused, "Focus is ", "after tab switch from no focus to no focus, first browser" ); focused = await getFocusedElementForBrowser(browser2, true); is( focused, "Focus is button2", "after tab switch from no focus to no focus, second browser" ); // next, check whether navigating forward, focusing the urlbar and then // navigating back maintains the focus in the urlbar. await expectFocusShift( () => focusElementInChild("button1", "focus"), "window1", "button1", true, "focus button" ); await promiseTabLoadEvent(tab1, "data:text/html," + escape(testPage3)); // now go back again gURLBar.focus(); await new Promise((resolve, reject) => { BrowserTestUtils.waitForContentEvent( window.gBrowser.selectedBrowser, "pageshow", true ).then(() => resolve()); document.getElementById("Browser:Back").doCommand(); }); is( window.document.activeElement, gURLBar.inputField, "urlbar still focused after navigating back" ); for (let listener of listenersToRemove) { listener(); } window.removeEventListener( "focus", _browser_tabfocus_test_eventOccured, true ); window.removeEventListener("blur", _browser_tabfocus_test_eventOccured, true); gBrowser.removeCurrentTab(); gBrowser.removeCurrentTab(); finish(); }); function _browser_tabfocus_test_eventOccured(event) { function getWindowDocId(target) { if ( target == browser1.contentWindow || target == browser1.contentDocument ) { return "window1"; } if ( target == browser2.contentWindow || target == browser2.contentDocument ) { return "window2"; } return "main-window"; } var id; if (Window.isInstance(event.target)) { id = getWindowDocId(event.originalTarget) + "-window"; } else if (Document.isInstance(event.target)) { id = getWindowDocId(event.originalTarget) + "-document"; } else if ( event.target.id == "urlbar" && event.originalTarget.localName == "input" ) { id = "urlbar"; } else if (event.originalTarget.localName == "browser") { id = event.originalTarget == browser1 ? "browser1" : "browser2"; } else if (event.originalTarget.localName == "tab") { id = event.originalTarget == tab1 ? "tab1" : "tab2"; } else { id = event.originalTarget.id; } actualEvents.push(event.type + ": " + id); compareFocusResults(); } function getId(element) { if (!element) { return null; } if (element.localName == "browser") { return element == browser1 ? "browser1" : "browser2"; } if (element.localName == "tab") { return element == tab1 ? "tab1" : "tab2"; } return element.localName == "input" ? "urlbar" : element.id; } async function compareFocusResults() { if (!currentPromiseResolver) { return; } // Get the events that occurred in each child browser and store them // in 'actualEvents'. This is a global so if different calls to // compareFocusResults occur together, whichever one happens to get // called first after pulling all the events from the child will // perform the matching. let events = await SpecialPowers.spawn(browser1, [], () => { let eventsOccurred = content._eventsOccurred; content._eventsOccurred = []; return eventsOccurred || []; }); actualEvents.window1.push(...events); events = await SpecialPowers.spawn(browser2, [], () => { let eventsOccurred = content._eventsOccurred; content._eventsOccurred = []; return eventsOccurred || []; }); actualEvents.window2.push(...events); // Another call to compareFocusResults may have happened in the meantime. // If currentPromiseResolver is null, then that call was successful so no // need to check the events again. if (!currentPromiseResolver) { return; } let winIds = ["main-window", "window1", "window2"]; for (let winId of winIds) { if (actualEvents[winId].length < expectedEvents[winId].length) { return; } } for (let winId of winIds) { for (let e = 0; e < expectedEvents.length; e++) { is( actualEvents[winId][e], expectedEvents[winId][e], currentTestName + " events [event " + e + "]" ); } actualEvents[winId] = []; } let matchWindow = window; is(_expectedWindow, "main-window", "main-window is always expected"); if (_expectedWindow == "main-window") { // The browser window's body doesn't have an id set usually - set one now // so it can be used for id comparisons below. matchWindow.document.body.id = "main-window-body"; } var focusedElement = fm.focusedElement; is( getId(focusedElement), _expectedElement, currentTestName + " focusedElement" ); is(fm.focusedWindow, matchWindow, currentTestName + " focusedWindow"); var focusedWindow = {}; is( getId(fm.getFocusedElementForWindow(matchWindow, false, focusedWindow)), _expectedElement, currentTestName + " getFocusedElementForWindow" ); is( focusedWindow.value, matchWindow, currentTestName + " getFocusedElementForWindow frame" ); is(matchWindow.document.hasFocus(), true, currentTestName + " hasFocus"); var expectedActive = _expectedElement; if (!expectedActive) { expectedActive = getId(matchWindow.document.body); } is( getId(matchWindow.document.activeElement), expectedActive, currentTestName + " activeElement" ); currentPromiseResolver(); currentPromiseResolver = null; } async function expectFocusShiftAfterTabSwitch( tab, expectedWindow, expectedElement, focusChanged, testid ) { let tabSwitchPromise = null; await expectFocusShift( () => { tabSwitchPromise = BrowserTestUtils.switchTab(gBrowser, tab); }, expectedWindow, expectedElement, focusChanged, testid ); await tabSwitchPromise; } async function expectFocusShift( callback, expectedWindow, expectedElement, focusChanged, testid ) { currentPromiseResolver = null; currentTestName = testid; expectedEvents = new EventStore(); if (focusChanged) { _expectedElement = expectedElement; _expectedWindow = expectedWindow; // When the content is in a child process, the expected element in the chrome window // will always be the urlbar or a browser element. if (_expectedWindow == "window1") { _expectedElement = "browser1"; } else if (_expectedWindow == "window2") { _expectedElement = "browser2"; } _expectedWindow = "main-window"; if ( _lastfocuswindow != "main-window" && _lastfocuswindow != expectedWindow ) { let browserid = _lastfocuswindow == "window1" ? "browser1" : "browser2"; expectedEvents.push("blur: " + browserid); } var newElementIsFocused = expectedElement && !expectedElement.startsWith("html"); if ( newElementIsFocused && _lastfocuswindow != "main-window" && expectedWindow == "main-window" ) { // When switching from a child to a chrome element, the focus on the element will arrive first. expectedEvents.push("focus: " + expectedElement); newElementIsFocused = false; } if (_lastfocus && _lastfocus != _expectedElement) { expectedEvents.push("blur: " + _lastfocus); } if (_lastfocuswindow && _lastfocuswindow != expectedWindow) { if (_lastfocuswindow != "main-window") { expectedEvents.push("blur: " + _lastfocuswindow + "-document"); expectedEvents.push("blur: " + _lastfocuswindow + "-window"); } } if (expectedWindow && _lastfocuswindow != expectedWindow) { if (expectedWindow != "main-window") { let browserid = expectedWindow == "window1" ? "browser1" : "browser2"; expectedEvents.push("focus: " + browserid); } if (expectedWindow != "main-window") { expectedEvents.push("focus: " + expectedWindow + "-document"); expectedEvents.push("focus: " + expectedWindow + "-window"); } } if (newElementIsFocused) { expectedEvents.push("focus: " + expectedElement); } _lastfocus = expectedElement; _lastfocuswindow = expectedWindow; } // No events are expected, so return immediately. If events do occur, the following // tests will fail. if ( expectedEvents["main-window"].length + expectedEvents.window1.length + expectedEvents.window2.length == 0 ) { await callback(); return undefined; } return new Promise(resolve => { currentPromiseResolver = resolve; callback(); }); }