Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* Any copyright is dedicated to the Public Domain.
"use strict";
/**
* Tests that the Back button in the search results header is visible
* whenever a search is active (both with and without results), and that
* activating it clears the search, returns to the previously focused
* category, and moves keyboard focus back to the search input.
*/
const RESULTS_QUERY = "privacy";
const NO_RESULTS_QUERY = "zzznothingtofindzzz";
/**
* Waits for `el` to become `doc.activeElement`. Uses `focusin` rather than
* `focus` because both `moz-input-search` and `moz-button` host their real
* focusable element inside a shadow root: the non-composed `focus` event
* stays trapped there, while `focusin` is composed and bubbles out. The
* check-first guard makes the helper safe when the element is already focused.
*/
async function promiseFocused(el, msg) {
let doc = el.ownerDocument;
if (doc.activeElement !== el) {
await BrowserTestUtils.waitForEvent(el, "focusin");
}
is(doc.activeElement, el, msg);
}
async function openSearchWithQuery(query) {
await openPreferencesViaOpenPreferencesAPI(DEFAULT_PANE, {
leaveOpen: true,
});
let doc = gBrowser.contentDocument;
let searchCompletedPromise = BrowserTestUtils.waitForEvent(
gBrowser.contentWindow,
"PreferencesSearchCompleted",
evt => evt.detail == query
);
doc.getElementById("searchInput").focus();
EventUtils.sendString(query);
await searchCompletedPromise;
return doc;
}
add_task(async function back_button_visible_with_results() {
let doc = await openSearchWithQuery(RESULTS_QUERY);
let win = gBrowser.contentWindow;
let searchInput = doc.getElementById("searchInput");
let srHeader = doc.getElementById("header-searchResults");
is_element_visible(srHeader, "Search results header is visible");
let backButton = doc.getElementById("search-results-back-button");
is_element_visible(
backButton,
"Back button is visible when results are found"
);
// The fix for this bug is that the Back button must NOT inherit tabindex=-1
// from the allownoselection nav mechanism. moz-button uses delegatesFocus so
// the host's .tabIndex is always -1; Tab-reachability is the correct signal.
searchInput.focus();
EventUtils.synthesizeKey("VK_TAB", {}, win);
await promiseFocused(
backButton,
"Tab from search input reaches the back button"
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
/**
* Back button is visible in the search results header even when no results
* are found. This is the worst-case accessibility scenario: no result rows
* to tab through, so the Back button is the only keyboard escape route.
*/
add_task(async function back_button_visible_with_no_results() {
let doc = await openSearchWithQuery(NO_RESULTS_QUERY);
let srHeader = doc.getElementById("header-searchResults");
is_element_visible(
srHeader,
"Search results header is visible even with no results"
);
let backButton = doc.getElementById("search-results-back-button");
is_element_visible(
backButton,
"Back button is visible when there are no results"
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
add_task(async function back_button_clears_search_and_focuses_search_input() {
let doc = await openSearchWithQuery(RESULTS_QUERY);
let srHeader = doc.getElementById("header-searchResults");
let searchInput = doc.getElementById("searchInput");
let backButton = doc.getElementById("search-results-back-button");
let categories = doc.getElementById("categories");
let searchCompletedPromise = BrowserTestUtils.waitForEvent(
gBrowser.contentWindow,
"PreferencesSearchCompleted",
evt => evt.detail == ""
);
synthesizeClick(backButton);
await searchCompletedPromise;
is(searchInput.value, "", "Search input is cleared after Back");
is_element_hidden(srHeader, "Search results header is hidden after Back");
await promiseFocused(
searchInput,
"Focus returns to the search input after Back"
);
is(
categories.currentView,
DEFAULT_PANE,
`Nav returns to the default pane (${DEFAULT_PANE}) after Back`
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
add_task(async function clearing_search_input_focuses_search_input() {
let doc = await openSearchWithQuery(RESULTS_QUERY);
let win = gBrowser.contentWindow;
let searchInput = doc.getElementById("searchInput");
let searchCompletedPromise = BrowserTestUtils.waitForEvent(
win,
"PreferencesSearchCompleted",
evt => evt.detail == ""
);
searchInput.focus();
EventUtils.synthesizeKey("a", { accelKey: true }, win);
EventUtils.synthesizeKey("KEY_Delete", {}, win);
await searchCompletedPromise;
await promiseFocused(
searchInput,
"Focus stays on the search input after clearing the query"
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
add_task(async function back_returns_to_non_default_category() {
await openPreferencesViaOpenPreferencesAPI(DEFAULT_PANE, { leaveOpen: true });
let doc = gBrowser.contentDocument;
let win = gBrowser.contentWindow;
let categories = doc.getElementById("categories");
let searchInput = doc.getElementById("searchInput");
let backButton = doc.getElementById("search-results-back-button");
const OTHER_PANE = "panePrivacy";
await win.gotoPref(OTHER_PANE);
is(categories.currentView, OTHER_PANE, "Selected a non-default category");
let resultsShown = BrowserTestUtils.waitForEvent(
win,
"PreferencesSearchCompleted",
evt => evt.detail == RESULTS_QUERY
);
searchInput.focus();
EventUtils.sendString(RESULTS_QUERY);
await resultsShown;
let backDone = BrowserTestUtils.waitForEvent(
win,
"PreferencesSearchCompleted",
evt => evt.detail == ""
);
synthesizeClick(backButton);
await backDone;
await promiseFocused(
searchInput,
"Focus returns to the search input after Back"
);
is(
categories.currentView,
OTHER_PANE,
"Back returns to the originally selected category, not the default"
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
add_task(async function back_button_keyboard_navigation_no_results() {
await openPreferencesViaOpenPreferencesAPI(DEFAULT_PANE, { leaveOpen: true });
let doc = gBrowser.contentDocument;
let win = gBrowser.contentWindow;
let categories = doc.getElementById("categories");
let searchInput = doc.getElementById("searchInput");
let srHeader = doc.getElementById("header-searchResults");
let backButton = doc.getElementById("search-results-back-button");
const OTHER_PANE = "panePrivacy";
await win.gotoPref(OTHER_PANE);
is(categories.currentView, OTHER_PANE, "Selected a non-default category");
let resultsShown = BrowserTestUtils.waitForEvent(
win,
"PreferencesSearchCompleted",
evt => evt.detail == NO_RESULTS_QUERY
);
searchInput.focus();
EventUtils.sendString(NO_RESULTS_QUERY);
await resultsShown;
// Tab from search input to back button (no result rows present).
EventUtils.synthesizeKey("VK_TAB", {}, win);
await promiseFocused(backButton, "Focus moves to the back button via Tab");
let backDone = BrowserTestUtils.waitForEvent(
win,
"PreferencesSearchCompleted",
evt => evt.detail == ""
);
EventUtils.synthesizeKey(" ", {}, win);
await backDone;
is(searchInput.value, "", "Search input is cleared after keyboard Back");
await promiseFocused(
searchInput,
"Focus returns to the search input after keyboard Back"
);
is_element_hidden(
srHeader,
"Search results header is hidden after keyboard Back"
);
is(
categories.currentView,
OTHER_PANE,
"Back returns to the originally selected category, not the default"
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
});