Commit e04542ea authored by Regan Hsu's avatar Regan Hsu Committed by Commit Bot

[OsSettingsSearch] Barebone components needed to show search result text

NOTE: There are still unresolved A11y issues. The current A11y follows
the same rules for the chrome browser search input. See
https://drive.google.com/file/d/1aXwE_2OJb6uMqhiyKLFOCyQ_c0sqrLxM/view

* Hides old search UI and shows new search UI when flag is on.
* Produces a list of random results when user inputs into search bar.
* Basic stylization to account for when screen is narrow mode.
* JS handling for keyboard and mouse events, highlighting correct row.
* Temporary stylizations and functions for testing.
* Rows are not actionable yet but are forced to via tabindex.
* Modified os settings ui and os toolbar so that the old search UI is
  not impacted but provides permissive environment for new UI.
* Tests.

Screenshots:
* Regular (wide) mode: https://screenshot.googleplex.com/9zZVYXoOzzQ
* Narrow mode: https://screenshot.googleplex.com/tpR1jMg0Cjv
* Narrow mode (not showing search):
  https://screenshot.googleplex.com/Labxcscu47U

Bug: 1056909
Change-Id: I5d8ae7313cfb799ead69dad6ffb06f4c4bcc4f8c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2099468Reviewed-by: default avatardpapad <dpapad@chromium.org>
Reviewed-by: default avatarKyle Horimoto <khorimoto@chromium.org>
Reviewed-by: default avatarjimmy gong <jimmyxgong@chromium.org>
Commit-Queue: Regan Hsu <hsuregan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#753786}
parent 0b986a3f
......@@ -108,6 +108,7 @@ group("closure_compile") {
"os_settings_main:closure_compile",
"os_settings_menu:closure_compile",
"os_settings_page:closure_compile",
"os_settings_search_box:closure_compile",
"os_settings_ui:closure_compile",
"parental_controls_page:closure_compile",
"personalization_page:closure_compile",
......
# Copyright 2020 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("//third_party/closure_compiler/compile_js.gni")
js_type_check("closure_compile") {
deps = [
":os_search_result_row",
":os_settings_search_box",
]
}
js_library("os_settings_search_box") {
deps = [
"..:metrics_recorder",
"//third_party/polymer/v1_0/components-chromium/iron-dropdown:iron-dropdown-extracted",
"//third_party/polymer/v1_0/components-chromium/iron-list:iron-list-extracted",
"//ui/webui/resources/cr_elements/cr_toolbar:cr_toolbar_search_field",
"//ui/webui/resources/js:assert",
]
}
js_library("os_search_result_row") {
deps = [
"//ui/webui/resources/js/cr/ui:focus_row",
"//ui/webui/resources/js/cr/ui:focus_row_behavior",
]
}
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/html/cr/ui/focus_row_behavior.html">
<link rel="import" href="../../settings_shared_css.html">
<dom-module id="os-search-result-row">
<template>
<style include="settings-shared">
:host {
display: flex;
flex-basis: 100%;
height: 40px;
}
:host([selected]) .search-result-container {
background-color: var(--cros-menu-button-bg-color-active);
}
:host(:not([selected])) .search-result-container:hover {
background-color: var(--cros-menu-button-bg-color-hover);
}
.search-result-container {
display: flex;
flex-basis: 100%;
}
.text {
flex-basis: 100%;
}
</style>
<div class="search-result-container" focus-row-container>
<!-- TODO(crbug/1056909): Focus right hand icon instead; move
focus-row-control to that icon when available-->
<div class="text" focus-type="rowWrapper" focus-row-control selectable>
[[searchResultText]]
</div>
</div>
</template>
<script src="os_search_result_row.js"></script>
</dom-module>
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview 'os-search-result-row' is the container for one search result.
*/
Polymer({
is: 'os-search-result-row',
behaviors: [cr.ui.FocusRowBehavior],
properties: {
// Whether the search result row is selected.
selected: {
type: Boolean,
reflectToAttribute: true,
},
// String to be displayed as a result in the UI.
searchResultText: String,
},
});
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/cr_elements/cr_toolbar/cr_toolbar_search_field.html">
<link rel="import" href="chrome://resources/html/assert.html">
<link rel="import" href="chrome://resources/html/cr/ui/focus_row.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-dropdown/iron-dropdown.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-list/iron-list.html">
<link rel="import" href="os_search_result_row.html">
<link rel="import" href="../metrics_recorder.html">
<link rel="import" href="../../settings_shared_css.html">
<dom-module id="os-settings-search-box">
<template>
<style include="settings-shared">
:host {
display: flex;
flex-basis: var(--cr-toolbar-field-width, 680px);
transition: width 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* Only search icon is visible in this mode */
:host([narrow]:not([showing-search])) {
justify-content: flex-end;
}
:host([narrow][showing-search]) {
flex-basis: 100%;
}
:host([narrow]) iron-dropdown {
left: 0;
right: 0;
}
iron-dropdown [slot='dropdown-content'] {
background-color: var(--cr-card-background-color);
box-shadow: var(--cr-card-shadow);
display: table;
}
:host([narrow]) iron-dropdown [slot='dropdown-content'] {
width: 100%;
}
:host(:not([narrow])) iron-dropdown [slot='dropdown-content'] {
width: var(--cr-toolbar-field-width, 680px);
}
</style>
<cr-toolbar-search-field id="search" narrow="[[narrow]]"
label="$i18n{searchPrompt}" clear-label="$i18n{clearSearch}"
showing-search="{{showingSearch}}" spinner-active="[[spinnerActive]]">
</cr-toolbar-search-field>
<iron-dropdown id="searchResults" opened="[[shouldShowDropdown_]]"
no-overlap allow-outside-scroll no-cancel-on-outside-click>
<!-- As part of iron-dropdown's behavior, the slot 'dropdown-content' is
hidden until iron-dropdown's opened attribute is set true, or when
iron-dropdown's open() is called on the JS side. -->
<iron-list id="searchResultList" slot="dropdown-content"
selection-enabled items="[[searchResults_]]"
selected-item="{{selectedItem}}">
<!-- TODO(crbug/1056909): Use template dom-if if searchResults_ is
empty, show 'No Results' UI -->
<template>
<os-search-result-row actionable search-result-text="[[item]]"
selected="[[isItemSelected_(item, selectedItem)]]"
tabindex$="[[getRowTabIndex_(item, selectedItem)]]"
iron-list-tab-index$="[[getRowTabIndex_(item, selectedItem)]]"
last-focused="{{lastFocused_}}"
list-blurred="{{listBlurred_}}"
focus-row-index="[[index]]"
first$="[[!index]]">
</os-search-result-row>
</template>
</iron-list>
</iron-dropdown>
</template>
<script src="os_settings_search_box.js"></script>
</dom-module>
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(function() {
'use strict';
/**
* @fileoverview 'os-settings-search-box' is the container for the search input
* and settings search results.
*/
/**
* Fake function to simulate async SettingsSearchHandler Search().
* TODO(crbug/1056909): Remove once Settings Search Handler mojo API is ready.
* @param {string} query The query used to fetch results.
* @return {!Promise<!Array<string>>} A promise that will resolve with an array
* of search results.
*/
function fakeSettingsSearchHandlerSearch(query) {
/**
* @param {number} min The lower bound integer.
* @param {number} max The upper bound integer.
* @return {number} A random integer between min and max inclusive.
*/
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
const fakeRandomResults = [
'bluetooth', 'wifi', 'language', 'people', 'personalization', 'security',
'touchpad', 'keyboard', 'passcode'
];
fakeRandomResults.sort(() => Math.random() - 0.5);
return new Promise(resolve => {
setTimeout(() => {
resolve(fakeRandomResults.splice(
0, getRandomInt(1, fakeRandomResults.length - 1)));
}, 0);
});
}
Polymer({
is: 'os-settings-search-box',
properties: {
// True when the toolbar is displaying in narrow mode.
// TODO(hsuregan): Change narrow to isNarrow here and associated elements.
narrow: {
type: Boolean,
reflectToAttribute: true,
},
// Controls whether the search field is shown.
showingSearch: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true,
},
// Value is proxied through to cr-toolbar-search-field. When true,
// the search field will show a processing spinner.
spinnerActive: Boolean,
// The currently selected search result associated with a
// <os-search-result-row>. This property is bound to <iron-list>.
// Note that when a item is selected, it's associated <os-search-result-row>
// is not focused() at the same time unless it is explicitly clicked/tapped.
selectedItem: {
// TODO(crbug/1056909): Change the type from String to Mojo result type.
type: String,
notify: true,
},
/**
* Passed into <iron-list>, one of which is always the selectedItem.
* @private {!Array<string>}
*/
searchResults_: {
type: Array,
notify: true,
observer: 'selectFirstRow_',
},
/** @private */
shouldShowDropdown_: {
type: Boolean,
value: false,
computed: 'computeShouldShowDropdown_(searchResults_)',
},
/**
* Used by FocusRowBehavior to track the last focused element inside a
* <os-search-result-row> with the attribute 'focus-row-control'.
* @private {HTMLElement}
*/
lastFocused_: Object,
/**
* Used by FocusRowBehavior to track if the list has been blurred.
* @private
*/
listBlurred_: Boolean,
},
/**
* Mojo OS Settings Search handler used to fetch search results.
* TODO(crbug/1056909): Set in ready() when Mojo bindings are ready or
* eliminate altogether along with fake JS file.
* @private {*}
*/
searchHandler_: undefined,
listeners: {
'blur': 'onBlur_',
'keydown': 'onKeyDown_',
'search-changed': 'fetchSearchResults_',
},
/** @private */
attached() {
const toolbarSearchField = this.$.search;
const searchInput = toolbarSearchField.getSearchInput();
searchInput.addEventListener(
'focus', this.onSearchInputFocused_.bind(this));
},
/**
* @param {*} searchHandler
*/
setSearchHandlerForTesting(searchHandler) {
this.searchHandler_ = searchHandler;
},
/**
* @return {boolean}
* @private
*/
computeShouldShowDropdown_() {
return this.searchResults_.length !== 0;
},
/** @private */
fetchSearchResults_() {
const query = this.$.search.getSearchInput().value;
if (query === '') {
this.searchResults_ = [];
return;
}
this.spinnerActive = true;
if (!this.searchHandler_) {
// TODO(crbug/1056909): Remove block when mojo interface is ready.
fakeSettingsSearchHandlerSearch(query).then(results => {
this.onSearchResultsReceived_(query, results);
});
return;
}
this.searchHandler_.search(query).then(results => {
this.onSearchResultsReceived_(query, results);
});
},
/**
* Updates search results UI when settings search results are fetched.
* @param {string} query The string used to find search results.
* @param {!Array<string>} results Array of search results.
* @private
*/
onSearchResultsReceived_(query, results) {
if (query !== this.$.search.getSearchInput().value) {
// Received search results are invalid as the query has since changed.
return;
}
this.spinnerActive = false;
this.lastFocused_ = null;
this.searchResults_ = results;
settings.recordSearch();
},
/** @private */
onBlur_() {
// The user has clicked a region outside the search box or the input has
// been blurred; close the dropdown regardless if there are searchResults_.
this.$.searchResults.close();
},
/** @private */
onSearchInputFocused_() {
this.lastFocused_ = null;
if (this.shouldShowDropdown_) {
// Restore previous results instead of re-fetching.
this.$.searchResults.open();
return;
}
this.fetchSearchResults_();
},
/**
* @param {string} item The search result item.
* @return {boolean} True if the item is selected.
* @private
*/
isItemSelected_(item) {
return this.searchResults_.indexOf(item) ===
this.searchResults_.indexOf(this.selectedItem);
},
/**
* Returns the correct tab index since <iron-list>'s default tabIndex property
* does not automatically add selectedItem's <os-search-result-row> to the
* default navigation flow, unless the user explicitly clicks on the row.
* @param {string} item The search result item.
* @return {number} A 0 if the row should be in the navigation flow, or a -1
* if the row should not be in the navigation flow.
* @private
*/
getRowTabIndex_(item) {
return this.isItemSelected_(item) ? 0 : -1;
},
/** @private */
selectFirstRow_() {
if (!this.shouldShowDropdown_) {
return;
}
this.selectedItem = this.searchResults_[0];
},
/**
* @param {string} key The string associated with a key.
* @private
*/
selectRowViaKeys_(key) {
assert(key === 'ArrowDown' || key === 'ArrowUp' || key === 'Tab');
assert(!!this.selectedItem, 'There should be a selected item already.');
// Select the new item.
const selectedRowIndex = this.searchResults_.indexOf(this.selectedItem);
const numRows = this.searchResults_.length;
const delta = key === 'ArrowUp' ? -1 : 1;
const indexOfNewRow = (numRows + selectedRowIndex + delta) % numRows;
this.selectedItem = this.searchResults_[indexOfNewRow];
// If a row is focused, ensure it is the selectedResult's row.
if (this.lastFocused_) {
const rowEls = Array.from(
this.$.searchResultList.querySelectorAll('os-search-result-row'));
// Calling focus() on a <os-search-result-row> focuses the element within
// containing the attribute 'focus-row-control'.
rowEls[indexOfNewRow].focus();
}
},
/**
* Keydown handler to specify how enter-key, arrow-up key, and arrow-down-key
* interacts with search results in the dropdown.
* @param {!KeyboardEvent} e
* @private
*/
onKeyDown_(e) {
if (!this.shouldShowDropdown_) {
// No action should be taken if there are no search results.
return;
}
if (e.key === 'Enter') {
// TODO(crbug/1056909): Take action on selected row.
return;
}
if (e.key === 'Tab') {
if (this.lastFocused_) {
// Prevent continuous tabbing from leaving search result
e.preventDefault();
this.selectRowViaKeys_(e.key);
}
return;
}
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
// Do not impact the position of <cr-toolbar-search-field>'s caret.
e.preventDefault();
this.selectRowViaKeys_(e.key);
return;
}
},
});
})();
......@@ -41,7 +41,9 @@
@apply --layout-center;
/* TODO(hsuregan): update for dark mode when needed. */
min-height: 56px;
z-index: 2;
/* Needs to be higher than #cr-container-show-top's z-index so that the
* new settings search dropdown is not struck through by the shadow. */
z-index: 3;
}
cr-drawer {
......
......@@ -248,6 +248,12 @@ Polymer({
this.showDropShadows();
}
if (loadTimeData.getBoolean('newOsSettingsSearch')) {
// TODO(crbug/1056909): Remove when new os settings search complete. This
// block prevents the old settings search code from being executed.
return;
}
const urlSearchQuery =
settings.Router.getInstance().getQueryParameters().get('search') || '';
if (urlSearchQuery == this.lastSearchQuery_) {
......@@ -258,7 +264,16 @@ Polymer({
const toolbar = /** @type {!OsToolbarElement} */ (this.$$('os-toolbar'));
const searchField =
/** @type {CrToolbarSearchFieldElement} */ (toolbar.getSearchField());
/** @type {?CrToolbarSearchFieldElement} */ (toolbar.getSearchField());
if (!searchField) {
// TODO(crbug/1056909): Remove this and surrounding code when new os
// settings search complete. If the search field has not been rendered
// yet, do not continue. crbug/1056909 changes the toolbar search field to
// an optional value, so the element is not attached to the DOM the first
// time this runs when the new OS Settings search flag is not flipped on.
return;
}
// If the search was initiated by directly entering a search URL, need to
// sync the URL parameter to the textbox.
......@@ -303,10 +318,14 @@ Polymer({
/**
* Handles the 'search-changed' event fired from the toolbar.
* TODO(crbug/1056909): Remove when new settings search complete.
* @param {!Event} e
* @private
*/
onSearchChanged_(e) {
if (loadTimeData.getBoolean('newOsSettingsSearch')) {
return;
}
const query = e.detail;
settings.Router.getInstance().navigateTo(
settings.routes.BASIC,
......
......@@ -9,6 +9,8 @@ js_type_check("closure_compile") {
}
js_library("os_toolbar") {
deps =
[ "//ui/webui/resources/cr_elements/cr_toolbar:cr_toolbar_search_field" ]
deps = [
"../os_settings_search_box",
"//ui/webui/resources/cr_elements/cr_toolbar:cr_toolbar_search_field",
]
}
......@@ -6,7 +6,9 @@
<link rel="import" href="chrome://resources/cr_elements/hidden_style_css.html">
<link rel="import" href="chrome://resources/cr_elements/icons.html">
<link rel="import" href="chrome://resources/cr_elements/shared_vars_css.html">
<link rel="import" href="chrome://resources/html/assert.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-media-query/iron-media-query.html">
<link rel="import" href="../os_settings_search_box/os_settings_search_box.html">
<dom-module id="os-toolbar">
<template>
......@@ -89,6 +91,10 @@
flex-basis: var(--settings-main-basis);
}
:host([narrow][showing-search_]) #rightContent {
display: none;
}
:host(:not([narrow])) #rightContent {
flex: 1 1 0;
text-align: end;
......@@ -108,11 +114,18 @@
</div>
<div id="centeredContent" hidden$="[[!showSearch]]">
<cr-toolbar-search-field id="search" narrow="[[narrow]]"
label="$i18n{searchPrompt}" clear-label="$i18n{clearSearch}"
spinner-active="[[spinnerActive]]"
showing-search="{{showingSearch_}}">
</cr-toolbar-search-field>
<template is="dom-if" if="[[!newOsSettingsSearch_]]">
<cr-toolbar-search-field id="search" narrow="[[narrow]]"
label="$i18n{searchPrompt}" clear-label="$i18n{clearSearch}"
spinner-active="[[spinnerActive]]"
showing-search="{{showingSearch_}}">
</cr-toolbar-search-field>
</template>
<template is="dom-if" if="[[newOsSettingsSearch_]]">
<os-settings-search-box id="searchBox" narrow="[[narrow]]"
showing-search="{{showingSearch_}}">
</os-settings-search-box>
</template>
<iron-media-query query="(max-width: [[narrowThreshold]]px)"
query-matches="{{narrow}}">
</iron-media-query>
......
......@@ -39,11 +39,25 @@ Polymer({
type: Boolean,
reflectToAttribute: true,
},
/** @private */
newOsSettingsSearch_: {
type: Boolean,
value() {
return loadTimeData.getBoolean('newOsSettingsSearch');
},
readOnly: true,
},
},
/** @return {!CrToolbarSearchFieldElement} */
/** @return {?CrToolbarSearchFieldElement} */
getSearchField() {
return this.$.search;
if (this.newOsSettingsSearch_) {
assertNotReached(
'New OS Search should not be using OsToolbar.getSearchField()');
}
return /** @type {?CrToolbarSearchFieldElement} */ (
this.$$('cr-toolbar-search-field'));
},
/** @private */
......
......@@ -617,6 +617,18 @@
<structure name="IDR_OS_SETTINGS_LANGUAGES_MANAGE_INPUT_METHODS_PAGE_JS"
file="chromeos/os_languages_page/manage_input_methods_page.js"
type="chrome_html" />
<structure name="IDR_OS_SETTINGS_OS_SEARCH_RESULT_ROW_JS"
file="chromeos/os_settings_search_box/os_search_result_row.js"
type="chrome_html" />
<structure name="IDR_OS_SETTINGS_OS_SEARCH_RESULT_ROW_HTML"
file="chromeos/os_settings_search_box/os_search_result_row.html"
type="chrome_html" />
<structure name="IDR_OS_SETTINGS_OS_SETTINGS_SEARCH_BOX_JS"
file="chromeos/os_settings_search_box/os_settings_search_box.js"
type="chrome_html" />
<structure name="IDR_OS_SETTINGS_OS_SETTINGS_SEARCH_BOX_HTML"
file="chromeos/os_settings_search_box/os_settings_search_box.html"
type="chrome_html" />
<structure name="IDR_OS_SETTINGS_OS_TOOLBAR_JS"
file="chromeos/os_toolbar/os_toolbar.js"
type="chrome_html" />
......
......@@ -1723,6 +1723,10 @@ void AddSearchInSettingsStrings(content::WebUIDataSource* html_source) {
l10n_util::GetStringFUTF16(
IDS_SETTINGS_SEARCH_NO_RESULTS_HELP,
base::ASCIIToUTF16(chrome::kOsSettingsSearchHelpURL)));
html_source->AddBoolean(
"newOsSettingsSearch",
base::FeatureList::IsEnabled(chromeos::features::kNewOsSettingsSearch));
}
void AddDateTimeStrings(content::WebUIDataSource* html_source) {
......
......@@ -94,6 +94,7 @@ CrElementsToolbarSearchFieldTest.prototype = {
/** @override */
extraLibraries: CrElementsBrowserTest.prototype.extraLibraries.concat([
'../test_util.js',
'cr_toolbar_search_field_tests.js',
]),
};
......
......@@ -5,7 +5,10 @@
import("//third_party/closure_compiler/compile_js.gni")
js_type_check("closure_compile") {
deps = [ ":fake_user_action_recorder" ]
deps = [
":fake_settings_search_handler",
":fake_user_action_recorder",
]
}
js_library("fake_user_action_recorder") {
......@@ -14,3 +17,7 @@ js_library("fake_user_action_recorder") {
"//ui/webui/resources/js:cr",
]
}
js_library("fake_settings_search_handler") {
deps = [ "//ui/webui/resources/js:cr" ]
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Fake implementation of SettingsSearchHandler for testing.
*/
cr.define('settings', function() {
/**
* Fake implementation of chromeos.settings.mojom.SettingsSearchHandlerRemote.
*/
class FakeSettingsSearchHandler {
constructor() {
/** @private {!Array<string>} */
this.fakeResults_ = [];
}
/**
* @param {!Array<string>} results fake results that will be returned
* when Search() is called.
*/
setFakeResults(results) {
this.fakeResults_ = results;
}
/**
* @param {string} query fake query used to compile search results.
*/
async search(query) {
return this.fakeResults_;
}
}
return {FakeSettingsSearchHandler: FakeSettingsSearchHandler};
});
......@@ -712,6 +712,29 @@ TEST_F('OSSettingsMainTest', 'AllJsTests', () => {
mocha.run();
});
// Tests for the new OS Settings Search Box
// eslint-disable-next-line no-var
var OSSettingsSearchBoxBrowserTest = class extends OSSettingsBrowserTest {
/** @override */
get featureList() {
return {enabled: ['chromeos::features::kNewOsSettingsSearch']};
}
/** @override */
get extraLibraries() {
return super.extraLibraries.concat([
BROWSER_SETTINGS_PATH + '../test_util.js',
'fake_settings_search_handler.js',
'fake_user_action_recorder.js',
'os_settings_search_box_test.js',
]);
}
};
TEST_F('OSSettingsSearchBoxBrowserTest', 'AllJsTests', () => {
mocha.run();
});
// Tests for the side-nav menu.
// eslint-disable-next-line no-var
var OSSettingsMenuTest = class extends OSSettingsBrowserTest {
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/** @fileoverview Runs tests for the OS settings search box. */
suite('OSSettingsSearchBox', () => {
/** @type {?OsToolbar} */
let toolbar;
/** @type {?OsSettingsSearchBox} */
let searchBox;
/** @type {?CrSearchFieldElement} */
let field;
/** @type {?IronDropdownElement} */
let dropDown;
/** @type {?IronListElement} */
let resultList;
/** @type {*} */
let settingsSearchHandler;
/** @type {?chromeos.settings.mojom.UserActionRecorderInterface} */
let userActionRecorder;
/** @param {string} term */
async function simulateSearch(term) {
field.$.searchInput.value = term;
field.onSearchTermInput();
field.onSearchTermSearch();
await settingsSearchHandler.search;
Polymer.dom.flush();
}
setup(function() {
toolbar = document.querySelector('os-settings-ui').$$('os-toolbar');
assertTrue(!!toolbar);
searchBox = toolbar.$$('os-settings-search-box');
assertTrue(!!searchBox);
field = searchBox.$$('cr-toolbar-search-field');
assertTrue(!!field);
dropDown = searchBox.$$('iron-dropdown');
assertTrue(!!dropDown);
resultList = searchBox.$$('iron-list');
assertTrue(!!resultList);
settingsSearchHandler = new settings.FakeSettingsSearchHandler();
searchBox.setSearchHandlerForTesting(settingsSearchHandler);
userActionRecorder = new settings.FakeUserActionRecorder();
settings.setUserActionRecorderForTesting(userActionRecorder);
});
teardown(async () => {
// Clear search field for next test.
await simulateSearch('');
settings.setUserActionRecorderForTesting(null);
searchBox.setSearchHandlerForTesting(undefined);
});
test('User action search event', async () => {
settingsSearchHandler.setFakeResults([]);
assertEquals(userActionRecorder.searchCount, 0);
await simulateSearch('query');
assertEquals(userActionRecorder.searchCount, 1);
});
test('Dropdown opens correctly when results are fetched', async () => {
// Closed dropdown if no results are returned.
settingsSearchHandler.setFakeResults([]);
assertFalse(dropDown.opened);
await simulateSearch('query 1');
assertFalse(dropDown.opened);
assertEquals(userActionRecorder.searchCount, 1);
// Open dropdown if results are returned.
settingsSearchHandler.setFakeResults(['result']);
await simulateSearch('query 2');
assertTrue(dropDown.opened);
});
test('Restore previous existing search results', async () => {
settingsSearchHandler.setFakeResults(['result 1']);
await simulateSearch('query');
assertTrue(dropDown.opened);
const resultRow = resultList.items[0];
// Child blur elements except field should not trigger closing of dropdown.
resultList.blur();
assertTrue(dropDown.opened);
dropDown.blur();
assertTrue(dropDown.opened);
// User clicks outside the search box, closing the dropdown.
searchBox.blur();
assertFalse(dropDown.opened);
// User clicks on input, restoring old results and opening dropdown.
field.$.searchInput.focus();
assertEquals('query', field.$.searchInput.value);
assertTrue(dropDown.opened);
// The same result row exists.
assertEquals(resultRow, resultList.items[0]);
// Search field is blurred, closing the dropdown.
field.$.searchInput.blur();
assertFalse(dropDown.opened);
// User clicks on input, restoring old results and opening dropdown.
field.$.searchInput.focus();
assertEquals('query', field.$.searchInput.value);
assertTrue(dropDown.opened);
// The same result row exists.
assertEquals(resultRow, resultList.items[0]);
});
test('Search result rows are selected correctly', async () => {
settingsSearchHandler.setFakeResults(['result 1', 'result 2']);
await simulateSearch('query');
assertTrue(dropDown.opened);
assertEquals(resultList.items.length, 2);
// The first row should be selected when results are fetched.
assertEquals(resultList.selectedItem, resultList.items[0]);
// Test ArrowUp and ArrowDown interaction with selecting.
const arrowUpEvent = new KeyboardEvent(
'keydown', {cancelable: true, key: 'ArrowUp', keyCode: 38});
const arrowDownEvent = new KeyboardEvent(
'keydown', {cancelable: true, key: 'ArrowDown', keyCode: 40});
// ArrowDown event should select next row.
searchBox.dispatchEvent(arrowDownEvent);
assertEquals(resultList.selectedItem, resultList.items[1]);
// If last row selected, ArrowDown brings select back to first row.
searchBox.dispatchEvent(arrowDownEvent);
assertEquals(resultList.selectedItem, resultList.items[0]);
// If first row selected, ArrowUp brings select back to last row.
searchBox.dispatchEvent(arrowUpEvent);
assertEquals(resultList.selectedItem, resultList.items[1]);
// ArrowUp should bring select previous row.
searchBox.dispatchEvent(arrowUpEvent);
assertEquals(resultList.selectedItem, resultList.items[0]);
// Test that ArrowLeft and ArrowRight do nothing.
const arrowLeftEvent = new KeyboardEvent(
'keydown', {cancelable: true, key: 'ArrowLeft', keyCode: 37});
const arrowRightEvent = new KeyboardEvent(
'keydown', {cancelable: true, key: 'ArrowRight', keyCode: 39});
// No change on ArrowLeft
searchBox.dispatchEvent(arrowLeftEvent);
assertEquals(resultList.selectedItem, resultList.items[0]);
// No change on ArrowRight
searchBox.dispatchEvent(arrowRightEvent);
assertEquals(resultList.selectedItem, resultList.items[0]);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment