Commit a3172608 authored by Evan Stade's avatar Evan Stade Committed by Commit Bot

Revert "Tab Search: Infinite list component"

This reverts commit 74dbe648.

Reason for revert: suspected of making TabSearchAppTest.All flaky
see crbug.com/1148009

Original change's description:
> Tab Search: Infinite list component
>
> A list component that renders DOM items on demand based on the viewable
> scroll area and user navigation and scrolling interactions. Provides a
> performance gain for use cases where users have several dozens of items
> that should be available while scrolling the list by deferring scripting
> and rendering costs until necessary to fill the current scroll view.
>
> Bug: 1099917
> Change-Id: Ib22c0edf5ebc5febe3d8d6d4f7d8ac319e9bd0a2
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2503401
> Commit-Queue: Roman Arora <romanarora@google.com>
> Reviewed-by: Thomas Lukaszewicz <tluk@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#826287}

TBR=tluk@chromium.org,romanarora@chromium.org,romanarora@google.com

Change-Id: I5bb4c3d348ad739d2d503522dc84d8654c21f117
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: 1099917
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2532987Reviewed-by: default avatarEvan Stade <estade@chromium.org>
Commit-Queue: Evan Stade <estade@chromium.org>
Cr-Commit-Position: refs/heads/master@{#826439}
parent 15c9a651
...@@ -54,7 +54,6 @@ preprocess_grit("preprocess_web_components") { ...@@ -54,7 +54,6 @@ preprocess_grit("preprocess_web_components") {
out_manifest = "$target_gen_dir/$preprocess_web_components_manifest" out_manifest = "$target_gen_dir/$preprocess_web_components_manifest"
in_files = [ in_files = [
"app.js", "app.js",
"infinite_list.js",
"tab_search_item.js", "tab_search_item.js",
"tab_search_search_field.js", "tab_search_search_field.js",
] ]
...@@ -104,7 +103,6 @@ js_type_check("closure_compile") { ...@@ -104,7 +103,6 @@ js_type_check("closure_compile") {
deps = [ deps = [
":app", ":app",
":fuzzy_search", ":fuzzy_search",
":infinite_list",
":tab_data", ":tab_data",
":tab_search_api_proxy", ":tab_search_api_proxy",
":tab_search_item", ":tab_search_item",
...@@ -120,6 +118,7 @@ js_library("app") { ...@@ -120,6 +118,7 @@ js_library("app") {
":tab_search_item", ":tab_search_item",
":tab_search_search_field", ":tab_search_search_field",
"//third_party/polymer/v3_0/components-chromium/iron-a11y-announcer", "//third_party/polymer/v3_0/components-chromium/iron-a11y-announcer",
"//third_party/polymer/v3_0/components-chromium/iron-selector:iron-selector",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled", "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
"//ui/webui/resources/js:cr.m", "//ui/webui/resources/js:cr.m",
"//ui/webui/resources/js:load_time_data.m", "//ui/webui/resources/js:load_time_data.m",
...@@ -135,13 +134,6 @@ js_library("fuzzy_search") { ...@@ -135,13 +134,6 @@ js_library("fuzzy_search") {
] ]
} }
js_library("infinite_list") {
deps = [
"//third_party/polymer/v3_0/components-chromium/iron-selector:iron-selector",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
]
}
js_library("tab_data") { js_library("tab_data") {
deps = [] deps = []
} }
...@@ -174,7 +166,6 @@ js_library("tab_search_search_field") { ...@@ -174,7 +166,6 @@ js_library("tab_search_search_field") {
html_to_js("web_components") { html_to_js("web_components") {
js_files = [ js_files = [
"app.js", "app.js",
"infinite_list.js",
"tab_search_item.js", "tab_search_item.js",
"tab_search_search_field.js", "tab_search_search_field.js",
] ]
......
<style include="mwb-shared-style"> <style include="mwb-shared-style">
#tabsList { #tabs {
--list-max-height: 280px; max-height: 280px;
overflow-x: hidden;
overflow-y: auto;
position: relative;
}
#tabsContainer {
height: calc(var(--mwb-item-height) * var(--item-count, 0));
} }
#no-results { #no-results {
...@@ -55,17 +62,20 @@ ...@@ -55,17 +62,20 @@
label="$i18n{searchTabs}" on-focus="onSearchFocus_" label="$i18n{searchTabs}" on-focus="onSearchFocus_"
on-keydown="onSearchKeyDown_" on-search-changed="onSearchChanged_"> on-keydown="onSearchKeyDown_" on-search-changed="onSearchChanged_">
</tab-search-search-field> </tab-search-search-field>
<div hidden="[[!filteredOpenTabs_.length]]"> <div id="tabs">
<infinite-list id="tabsList" items="[[filteredOpenTabs_]]" > <div id="tabsContainer">
<template> <iron-selector id="selector" on-keydown="onItemKeyDown_"
<tab-search-item id="[[item.tab.tabId]]" aria-label="[[ariaLabel_(item)]]" selected="{{selectedIndex_}}" selected-class="selected" role="listbox">
class="mwb-list-item" data="[[item]]" <template id="tabsList" is="dom-repeat" items="[[filteredOpenTabs_]]"
on-click="onItemClick_" on-close="onItemClose_" initial-count="[[chunkingItemCount_]]">
on-focus="onItemFocus_" on-keydown="onItemKeyDown_" tabindex="0" <tab-search-item id="[[item.tab.tabId]]" aria-label="[[ariaLabel_(item)]]"
role="option"> class="mwb-list-item" data="[[item]]"
</tab-search-item> on-click="onItemClick_" on-close="onItemClose_"
</template> on-focus="onItemFocus_" tabindex="0" role="option">
</infinite-list> </tab-search-item>
</template>
</iron-selector>
</div>
</div> </div>
<div id="no-results" hidden="[[filteredOpenTabs_.length]]"> <div id="no-results" hidden="[[filteredOpenTabs_.length]]">
$i18n{noResultsFound} $i18n{noResultsFound}
......
...@@ -8,22 +8,24 @@ import 'chrome://resources/cr_elements/mwb_shared_style.js'; ...@@ -8,22 +8,24 @@ import 'chrome://resources/cr_elements/mwb_shared_style.js';
import 'chrome://resources/cr_elements/mwb_shared_vars.js'; import 'chrome://resources/cr_elements/mwb_shared_vars.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js'; import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import 'chrome://resources/polymer/v3_0/iron-iconset-svg/iron-iconset-svg.js'; import 'chrome://resources/polymer/v3_0/iron-iconset-svg/iron-iconset-svg.js';
import './infinite_list.js'; import 'chrome://resources/polymer/v3_0/iron-selector/iron-selector.js';
import './tab_search_item.js'; import './tab_search_item.js';
import './tab_search_search_field.js'; import './tab_search_search_field.js';
import './strings.js'; import './strings.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js'; import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {listenOnce} from 'chrome://resources/js/util.m.js'; import {listenOnce} from 'chrome://resources/js/util.m.js';
import {IronA11yAnnouncer} from 'chrome://resources/polymer/v3_0/iron-a11y-announcer/iron-a11y-announcer.js'; import {IronA11yAnnouncer} from 'chrome://resources/polymer/v3_0/iron-a11y-announcer/iron-a11y-announcer.js';
import {afterNextRender, html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {fuzzySearch} from './fuzzy_search.js'; import {fuzzySearch} from './fuzzy_search.js';
import {InfiniteList, NO_SELECTION, selectorNavigationKeys} from './infinite_list.js';
import {TabData} from './tab_data.js'; import {TabData} from './tab_data.js';
import {Tab, WindowTabs} from './tab_search.mojom-webui.js'; import {Tab, WindowTabs} from './tab_search.mojom-webui.js';
import {TabSearchApiProxy, TabSearchApiProxyImpl} from './tab_search_api_proxy.js'; import {TabSearchApiProxy, TabSearchApiProxyImpl} from './tab_search_api_proxy.js';
const selectorNavigationKeys = ['ArrowUp', 'ArrowDown', 'Home', 'End'];
export class TabSearchAppElement extends PolymerElement { export class TabSearchAppElement extends PolymerElement {
static get is() { static get is() {
return 'tab-search-app'; return 'tab-search-app';
...@@ -53,6 +55,16 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -53,6 +55,16 @@ export class TabSearchAppElement extends PolymerElement {
value: [], value: [],
}, },
/**
* Controls the number of tab search list items initially rendered in
* dom-repeat's chunked rendering mode.
* @private {number}
*/
chunkingItemCount_: {
type: Number,
value: 10,
},
/** /**
* Options for fuzzy search. * Options for fuzzy search.
* @private {!Object} * @private {!Object}
...@@ -154,13 +166,26 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -154,13 +166,26 @@ export class TabSearchAppElement extends PolymerElement {
// Prior to the first load |this.openTabs_| has not been set. Record the // Prior to the first load |this.openTabs_| has not been set. Record the
// time it takes for the initial list of tabs to render. // time it takes for the initial list of tabs to render.
if (!this.openTabs_) { if (!this.openTabs_) {
listenOnce(this.$.tabsList, 'dom-change', () => { listenOnce(this.$.tabsList, 'rendered-item-count-changed', e => {
afterNextRender(this, () => { const event = /** @type {!CustomEvent<!{value: number}>} */ (e);
// The initial rendered tab list must be non-zero.
assert(event.detail.value > 0);
// Chunking is used to bound the time to interactive for users
// irrespective of the number of tabs they have open. This is no longer
// needed after the initial list render and can cause flickering on
// updates so disable it here.
// TODO(tluk): Investigate a more efficient way to handle this.
this.chunkingItemCount_ = 0;
// Push showUI() to the event loop to allow reflow to occur following
// the DOM update.
setTimeout(() => {
this.apiProxy_.showUI(); this.apiProxy_.showUI();
chrome.metricsPrivate.recordTime( chrome.metricsPrivate.recordTime(
'Tabs.TabSearch.WebUI.InitialTabsRenderTime', 'Tabs.TabSearch.WebUI.InitialTabsRenderTime',
Math.round(window.performance.now())); Math.round(window.performance.now()));
}); }, 0);
}); });
} }
this.openTabs_ = profileTabs.windows; this.openTabs_ = profileTabs.windows;
...@@ -209,7 +234,10 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -209,7 +234,10 @@ export class TabSearchAppElement extends PolymerElement {
* @return {number} * @return {number}
*/ */
getSelectedIndex() { getSelectedIndex() {
return /** @type {!InfiniteList} */ (this.$.tabsList).selected; const selector = /** @type {!IronSelectorElement} */ (this.$.selector);
return selector.selected !== undefined ?
/** @type {number} */ (selector.selected) :
-1;
} }
/** /**
...@@ -221,8 +249,9 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -221,8 +249,9 @@ export class TabSearchAppElement extends PolymerElement {
this.updateFilteredTabs_(this.openTabs_ || []); this.updateFilteredTabs_(this.openTabs_ || []);
// Reset the selected item whenever a search query is provided. // Reset the selected item whenever a search query is provided.
/** @type {!InfiniteList} */ (this.$.tabsList).selected = this.$.selector.selected =
this.filteredOpenTabs_.length > 0 ? 0 : NO_SELECTION; this.filteredOpenTabs_.length > 0 ? 0 : undefined;
this.$.tabs.scrollTop = 0;
const length = this.filteredOpenTabs_.length; const length = this.filteredOpenTabs_.length;
let text; let text;
...@@ -244,7 +273,7 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -244,7 +273,7 @@ export class TabSearchAppElement extends PolymerElement {
/** @private */ /** @private */
onFeedbackFocus_() { onFeedbackFocus_() {
/** @type {!InfiniteList} */ (this.$.tabsList).selected = NO_SELECTION; this.$.selector.selected = undefined;
} }
/** /**
...@@ -265,27 +294,11 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -265,27 +294,11 @@ export class TabSearchAppElement extends PolymerElement {
const tabId = Number.parseInt(e.currentTarget.id, 10); const tabId = Number.parseInt(e.currentTarget.id, 10);
this.apiProxy_.closeTab(tabId); this.apiProxy_.closeTab(tabId);
this.announceA11y_(loadTimeData.getString('a11yTabClosed')); this.announceA11y_(loadTimeData.getString('a11yTabClosed'));
listenOnce(this.$.tabsList, 'iron-items-changed', () => { listenOnce(this.$.tabsList, 'rendered-item-count-changed', () => {
performance.mark('close_tab:benchmark_end'); performance.mark('close_tab:benchmark_end');
}); });
} }
/**
* @param {!Event} e
* @private
*/
onItemKeyDown_(e) {
if (e.key !== 'Enter' && e.key !== ' ') {
return;
}
e.stopPropagation();
e.preventDefault();
this.apiProxy_.switchToTab(
{tabId: this.getSelectedTab_().tabId}, !!this.searchText_);
}
/** /**
* @param {!Array<!WindowTabs>} newOpenTabs * @param {!Array<!WindowTabs>} newOpenTabs
* @private * @private
...@@ -297,10 +310,9 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -297,10 +310,9 @@ export class TabSearchAppElement extends PolymerElement {
// selected; else retain the currently selected index. If the list // selected; else retain the currently selected index. If the list
// shrunk above the selected index, select the last index in the list. // shrunk above the selected index, select the last index in the list.
// If there are no matching results, set the selected index value to none. // If there are no matching results, set the selected index value to none.
const tabsList = /** @type {!InfiniteList} */ (this.$.tabsList); this.$.selector.selectIndex(Math.min(
tabsList.selected = Math.min(
Math.max(this.getSelectedIndex(), 0), Math.max(this.getSelectedIndex(), 0),
this.filteredOpenTabs_.length - 1); this.filteredOpenTabs_.length - 1));
} }
/** /**
...@@ -308,18 +320,67 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -308,18 +320,67 @@ export class TabSearchAppElement extends PolymerElement {
* @private * @private
*/ */
onItemFocus_(e) { onItemFocus_(e) {
// Ensure that when a TabSearchItem receives focus, it becomes the selected this.$.selector.selected =
// item in the list.
/** @type {!InfiniteList} */ (this.$.tabsList).selected =
e.currentTarget.parentNode.indexOf(e.currentTarget); e.currentTarget.parentNode.indexOf(e.currentTarget);
this.updateScrollView_();
}
/**
* @param {string} key Keyboard event key value.
* @private
*/
selectorNavigate_(key) {
const selector = /** @type {!IronSelectorElement} */ (this.$.selector);
switch (key) {
case 'ArrowUp':
selector.selectPrevious();
break;
case 'ArrowDown':
selector.selectNext();
break;
case 'Home':
selector.selected = 0;
break;
case 'End':
selector.selected = this.filteredOpenTabs_.length - 1;
break;
}
}
/**
* Handles key events when list item elements have focus.
* @param {!KeyboardEvent} e
* @private
*/
onItemKeyDown_(e) {
if (e.shiftKey) {
return;
}
if (this.getSelectedIndex() === -1) {
// No tabs matching the search text criteria.
return;
}
if (selectorNavigationKeys.includes(e.key)) {
this.selectorNavigate_(e.key);
/** @type {!HTMLElement} */ (this.$.selector.selectedItem).focus({
preventScroll: true
});
e.stopPropagation();
e.preventDefault();
} else if (e.key === 'Enter' || e.key === ' ') {
this.apiProxy_.switchToTab(
{tabId: this.getSelectedTab_().tabId}, !!this.searchText_);
e.stopPropagation();
}
} }
/** @private */ /** @private */
onSearchFocus_() { onSearchFocus_() {
const tabsList = /** @type {!InfiniteList} */ (this.$.tabsList); if (this.$.selector.selected === undefined &&
if (tabsList.selected === NO_SELECTION &&
this.filteredOpenTabs_.length > 0) { this.filteredOpenTabs_.length > 0) {
tabsList.selected = 0; this.$.selector.selectIndex(0);
} }
} }
...@@ -352,8 +413,8 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -352,8 +413,8 @@ export class TabSearchAppElement extends PolymerElement {
} }
if (selectorNavigationKeys.includes(e.key)) { if (selectorNavigationKeys.includes(e.key)) {
/** @type {!InfiniteList} */ (this.$.tabsList).navigate(e.key); this.selectorNavigate_(e.key);
this.updateScrollView_();
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
...@@ -416,6 +477,42 @@ export class TabSearchAppElement extends PolymerElement { ...@@ -416,6 +477,42 @@ export class TabSearchAppElement extends PolymerElement {
}); });
this.filteredOpenTabs_ = this.filteredOpenTabs_ =
fuzzySearch(this.searchText_, result, this.fuzzySearchOptions_); fuzzySearch(this.searchText_, result, this.fuzzySearchOptions_);
// Update the item count in css so that the css rule can calculate the final
// height of the tabsContainer. This prevents the scrolling height from
// changing as list items are added to the dom incrementally via chunking
// mode.
this.$.tabsContainer.style
.setProperty("--item-count", this.filteredOpenTabs_.length.toString());
}
/**
* Ensure the scroll view can fully display a preceding or following tab item
* if existing.
* @private
*/
updateScrollView_() {
const selectedIndex = this.getSelectedIndex();
if (selectedIndex === 0 ||
selectedIndex === this.filteredOpenTabs_.length - 1) {
/** @type {!Element} */ (this.$.selector.selectedItem).scrollIntoView({
behavior: 'smooth'
});
} else {
const previousItem = this.$.selector.items[this.$.selector.selected - 1];
if (previousItem.offsetTop < this.$.tabs.scrollTop) {
/** @type {!Element} */ (previousItem)
.scrollIntoView({behavior: 'smooth', block: 'nearest'});
return;
}
const nextItem = this.$.selector.items[this.$.selector.selected + 1];
if (nextItem.offsetTop + nextItem.offsetHeight >
this.$.tabs.scrollTop + this.$.tabs.offsetHeight) {
/** @type {!Element} */ (nextItem).scrollIntoView(
{behavior: 'smooth', block: 'nearest'});
}
}
} }
/** return {!Tab} */ /** return {!Tab} */
......
<style include="mwb-shared-style">
:host {
display: block;
}
#container {
max-height: var(--list-max-height);
overflow-x: hidden;
overflow-y: auto;
position: relative;
}
</style>
<div id="container" on-scroll="onScroll_">
<div id="items">
<iron-selector id="selector" on-keydown="onKeyDown_"
on-iron-items-changed="updateScrollerSize_"
on-iron-select="onSelectedChanged_" role="listbox"
selected-class="selected">
</iron-selector>
</div>
</div>
This diff is collapsed.
...@@ -61,7 +61,6 @@ class TabSearchUIBrowserTest : public InProcessBrowserTest { ...@@ -61,7 +61,6 @@ class TabSearchUIBrowserTest : public InProcessBrowserTest {
IN_PROC_BROWSER_TEST_F(TabSearchUIBrowserTest, InitialTabItemsListed) { IN_PROC_BROWSER_TEST_F(TabSearchUIBrowserTest, InitialTabItemsListed) {
const std::string tab_items_js = const std::string tab_items_js =
"const tabItems = document.querySelector('tab-search-app').shadowRoot" "const tabItems = document.querySelector('tab-search-app').shadowRoot"
" .getElementById('tabsList').shadowRoot"
" .querySelectorAll('tab-search-item');"; " .querySelectorAll('tab-search-item');";
int tab_item_count = int tab_item_count =
content::EvalJs(webui_contents_.get(), tab_items_js + "tabItems.length", content::EvalJs(webui_contents_.get(), tab_items_js + "tabItems.length",
...@@ -82,7 +81,7 @@ IN_PROC_BROWSER_TEST_F(TabSearchUIBrowserTest, SwitchToTabAction) { ...@@ -82,7 +81,7 @@ IN_PROC_BROWSER_TEST_F(TabSearchUIBrowserTest, SwitchToTabAction) {
const std::string tab_item_js = base::StringPrintf( const std::string tab_item_js = base::StringPrintf(
"document.querySelector('tab-search-app').shadowRoot" "document.querySelector('tab-search-app').shadowRoot"
" .getElementById('tabsList').shadowRoot.getElementById('%s')", " .getElementById('%s')",
base::NumberToString(tab_id).c_str()); base::NumberToString(tab_id).c_str());
ASSERT_TRUE(content::ExecJs(webui_contents_.get(), tab_item_js + ".click()", ASSERT_TRUE(content::ExecJs(webui_contents_.get(), tab_item_js + ".click()",
content::EXECUTE_SCRIPT_DEFAULT_OPTIONS, content::EXECUTE_SCRIPT_DEFAULT_OPTIONS,
...@@ -97,8 +96,7 @@ IN_PROC_BROWSER_TEST_F(TabSearchUIBrowserTest, CloseTabAction) { ...@@ -97,8 +96,7 @@ IN_PROC_BROWSER_TEST_F(TabSearchUIBrowserTest, CloseTabAction) {
browser()->tab_strip_model()->GetWebContentsAt(0)); browser()->tab_strip_model()->GetWebContentsAt(0));
const std::string tab_item_button_js = base::StringPrintf( const std::string tab_item_button_js = base::StringPrintf(
"document.querySelector('tab-search-app').shadowRoot" "document.querySelector('tab-search-app').shadowRoot.getElementById('%s')"
" .getElementById('tabsList').shadowRoot.getElementById('%s')"
" .shadowRoot.getElementById('closeButton')", " .shadowRoot.getElementById('closeButton')",
base::NumberToString(tab_id).c_str()); base::NumberToString(tab_id).c_str());
ASSERT_TRUE(content::ExecJs(webui_contents_.get(), ASSERT_TRUE(content::ExecJs(webui_contents_.get(),
......
...@@ -21,7 +21,6 @@ js_type_check("closure_compile") { ...@@ -21,7 +21,6 @@ js_type_check("closure_compile") {
] ]
deps = [ deps = [
":fuzzy_search_test", ":fuzzy_search_test",
":infinite_list_test",
":tab_search_app_focus_test", ":tab_search_app_focus_test",
":tab_search_app_test", ":tab_search_app_test",
":tab_search_item_test", ":tab_search_item_test",
...@@ -36,17 +35,11 @@ js_library("fuzzy_search_test") { ...@@ -36,17 +35,11 @@ js_library("fuzzy_search_test") {
externs_list = [ "$externs_path/mocha-2.5.js" ] externs_list = [ "$externs_path/mocha-2.5.js" ]
} }
js_library("infinite_list_test") {
deps = [ "..:chai_assert" ]
externs_list = [ "$externs_path/mocha-2.5.js" ]
}
js_library("tab_search_app_test") { js_library("tab_search_app_test") {
deps = [ deps = [
":test_tab_search_api_proxy", ":test_tab_search_api_proxy",
"..:chai_assert", "..:chai_assert",
"//chrome/browser/resources/tab_search:app", "//chrome/browser/resources/tab_search:app",
"//chrome/browser/resources/tab_search:infinite_list",
] ]
externs_list = [ "$externs_path/mocha-2.5.js" ] externs_list = [ "$externs_path/mocha-2.5.js" ]
} }
......
// 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 {InfiniteList} from 'chrome://tab-search/infinite_list.js';
import {TabSearchItem} from 'chrome://tab-search/tab_search_item.js';
import {assertEquals, assertGT, assertNotEquals, assertTrue} from '../../chai_assert.js';
import {flushTasks, waitAfterNextRender} from '../../test_util.m.js';
import {generateSampleTabsFromSiteNames, sampleSiteNames} from './tab_search_test_data.js';
import {assertTabItemAndNeighborsInViewBounds, disableScrollIntoViewAnimations} from './tab_search_test_helper.js';
suite('InfniteListTest', () => {
const CHUNK_ITEM_COUNT = 7;
/** @type {!InfiniteList} */
let infiniteList;
disableScrollIntoViewAnimations(TabSearchItem);
/**
* @param {!Array<!tabSearch.mojom.Tab>} sampleData
*/
async function setupTest(sampleData) {
document.head.insertAdjacentHTML(
'beforeend', `<style>html{--list-max-height: 280px}</style>`);
document.body.innerHTML = `
<infinite-list id="list" chunk-item-count="${CHUNK_ITEM_COUNT}">
<template>
<tab-search-item id="[[item.tab.tabId]]" class="mwb-list-item"
data="[[item]]" tabindex="0" role="option">
</tab-search-item>
</template>
</infinite-list>`;
infiniteList =
/** @type {!InfiniteList} */ (document.querySelector('#list'));
infiniteList.items = sampleData;
await flushTasks();
}
/**
* @return {!NodeList<!HTMLElement>}
*/
function queryRows() {
return /** @type {!NodeList<!HTMLElement>} */ (
infiniteList.shadowRoot.querySelectorAll('tab-search-item'));
}
/**
* @param {!Array<string>} siteNames
* @return {!Array}
*/
function sampleTabItems(siteNames) {
return generateSampleTabsFromSiteNames(siteNames).map(tab => {
return {hostname: new URL(tab.url).hostname, tab};
});
}
test('ScrollHeight', async () => {
const tabItems = sampleTabItems(sampleSiteNames());
await setupTest(tabItems);
const container = infiniteList.shadowRoot.getElementById('container');
assertEquals(0, container.scrollTop);
const itemHeightStyle =
getComputedStyle(document.head).getPropertyValue('--mwb-item-height');
assertTrue(itemHeightStyle.endsWith('px'));
const tabItemHeight = Number.parseInt(
itemHeightStyle.substring(0, itemHeightStyle.length - 2), 10);
assertEquals(tabItemHeight * tabItems.length, container.scrollHeight);
});
test('SelectedIndex', async () => {
const siteNames = Array.from({length: 50}, (_, i) => 'site' + (i + 1));
const tabItems = sampleTabItems(siteNames);
await setupTest(tabItems);
const container = infiniteList.shadowRoot.getElementById('container');
assertEquals(0, container.scrollTop);
assertEquals(CHUNK_ITEM_COUNT, queryRows().length);
// Assert that upon changing the selected index to a non previously rendered
// item, this one is rendered on the view.
infiniteList.selected = CHUNK_ITEM_COUNT;
assertEquals(CHUNK_ITEM_COUNT, queryRows().length);
await waitAfterNextRender(infiniteList);
let domTabItems = queryRows();
const selectedTabItem = domTabItems[infiniteList.selected];
assertNotEquals(null, selectedTabItem);
assertEquals(2 * CHUNK_ITEM_COUNT, domTabItems.length);
// Assert that the view scrolled to show the selected item.
const afterSelectionScrollTop = container.scrollTop;
assertNotEquals(0, afterSelectionScrollTop);
// Assert that on replacing the list items, the currently selected index
// value is still rendered on the view.
infiniteList.items = sampleTabItems(siteNames);
await waitAfterNextRender(infiniteList);
domTabItems = queryRows();
const theSelectedTabItem = domTabItems[infiniteList.selected];
assertNotEquals(null, theSelectedTabItem);
// Assert the selected item is still visible in the view.
assertEquals(afterSelectionScrollTop, container.scrollTop);
});
test('NavigateDownShowsPreviousAndFollowingListItems', async () => {
const tabItems = sampleTabItems(sampleSiteNames());
await setupTest(tabItems);
const tabsDiv = /** @type {!HTMLElement} */
(infiniteList.shadowRoot.querySelector('#container'));
// Assert that the tabs are in a overflowing state.
assertGT(tabsDiv.scrollHeight, tabsDiv.clientHeight);
infiniteList.selected = 0;
for (let i = 0; i < tabItems.length; i++) {
infiniteList.navigate('ArrowDown');
await waitAfterNextRender(infiniteList);
const selectedIndex = ((i + 1) % tabItems.length);
assertEquals(selectedIndex, infiniteList.selected);
assertTabItemAndNeighborsInViewBounds(
tabsDiv, queryRows(), selectedIndex);
}
});
test('NavigateUpShowsPreviousAndFollowingListItems', async () => {
const tabItems = sampleTabItems(sampleSiteNames());
await setupTest(tabItems);
const tabsDiv = /** @type {!HTMLElement} */
(infiniteList.shadowRoot.querySelector('#container'));
// Assert that the tabs are in a overflowing state.
assertGT(tabsDiv.scrollHeight, tabsDiv.clientHeight);
infiniteList.selected = 0;
for (let i = tabItems.length; i > 0; i--) {
infiniteList.navigate('ArrowUp');
await waitAfterNextRender(infiniteList);
const selectIndex = (i - 1 + tabItems.length) % tabItems.length;
assertEquals(selectIndex, infiniteList.selected);
assertTabItemAndNeighborsInViewBounds(tabsDiv, queryRows(), selectIndex);
}
});
});
...@@ -55,8 +55,7 @@ suite('TabSearchAppFocusTest', () => { ...@@ -55,8 +55,7 @@ suite('TabSearchAppFocusTest', () => {
assertEquals(searchInput, getDeepActiveElement()); assertEquals(searchInput, getDeepActiveElement());
const tabSearchItems = /** @type {!NodeList<!HTMLElement>} */ const tabSearchItems = /** @type {!NodeList<!HTMLElement>} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelectorAll('tab-search-item'));
.shadowRoot.querySelectorAll('tab-search-item'));
tabSearchItems[0].focus(); tabSearchItems[0].focus();
// Once an item is focused, arrow keys should change focus too. // Once an item is focused, arrow keys should change focus too.
...@@ -89,8 +88,7 @@ suite('TabSearchAppFocusTest', () => { ...@@ -89,8 +88,7 @@ suite('TabSearchAppFocusTest', () => {
await setupTest(sampleData()); await setupTest(sampleData());
const tabSearchItem = /** @type {!HTMLElement} */ const tabSearchItem = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelector('tab-search-item'));
.shadowRoot.querySelector('tab-search-item'));
tabSearchItem.focus(); tabSearchItem.focus();
keyDownOn(tabSearchItem, 0, [], 'Enter'); keyDownOn(tabSearchItem, 0, [], 'Enter');
...@@ -107,14 +105,12 @@ suite('TabSearchAppFocusTest', () => { ...@@ -107,14 +105,12 @@ suite('TabSearchAppFocusTest', () => {
await setupTest(generateSampleDataFromSiteNames(sampleSiteNames())); await setupTest(generateSampleDataFromSiteNames(sampleSiteNames()));
const tabsDiv = /** @type {!HTMLElement} */ const tabsDiv = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelector('#tabs'));
.shadowRoot.querySelector('#container'));
// Assert that the tabs are in a overflowing state. // Assert that the tabs are in a overflowing state.
assertGT(tabsDiv.scrollHeight, tabsDiv.clientHeight); assertGT(tabsDiv.scrollHeight, tabsDiv.clientHeight);
const tabItems = /** @type {!NodeList<HTMLElement>} */ const tabItems = /** @type {!NodeList<HTMLElement>} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelectorAll('tab-search-item'));
.shadowRoot.querySelectorAll('tab-search-item'));
for (let i = 0; i < tabItems.length; i++) { for (let i = 0; i < tabItems.length; i++) {
tabItems[i].focus(); tabItems[i].focus();
......
...@@ -9,11 +9,11 @@ import {TabSearchApiProxy, TabSearchApiProxyImpl} from 'chrome://tab-search/tab_ ...@@ -9,11 +9,11 @@ import {TabSearchApiProxy, TabSearchApiProxyImpl} from 'chrome://tab-search/tab_
import {TabSearchItem} from 'chrome://tab-search/tab_search_item.js'; import {TabSearchItem} from 'chrome://tab-search/tab_search_item.js';
import {TabSearchSearchField} from 'chrome://tab-search/tab_search_search_field.js'; import {TabSearchSearchField} from 'chrome://tab-search/tab_search_search_field.js';
import {assertEquals, assertFalse, assertNotEquals, assertTrue} from '../../chai_assert.js'; import {assertEquals, assertFalse, assertGT, assertNotEquals, assertTrue} from '../../chai_assert.js';
import {flushTasks, waitAfterNextRender} from '../../test_util.m.js'; import {flushTasks, waitAfterNextRender} from '../../test_util.m.js';
import {sampleData} from './tab_search_test_data.js'; import {generateSampleDataFromSiteNames, sampleData, sampleSiteNames} from './tab_search_test_data.js';
import {initLoadTimeDataWithDefaults} from './tab_search_test_helper.js'; import {assertTabItemAndNeighborsInViewBounds, assertTabItemInViewBounds, disableScrollIntoViewAnimations, initLoadTimeDataWithDefaults} from './tab_search_test_helper.js';
import {TestTabSearchApiProxy} from './test_tab_search_api_proxy.js'; import {TestTabSearchApiProxy} from './test_tab_search_api_proxy.js';
suite('TabSearchAppTest', () => { suite('TabSearchAppTest', () => {
...@@ -22,6 +22,8 @@ suite('TabSearchAppTest', () => { ...@@ -22,6 +22,8 @@ suite('TabSearchAppTest', () => {
/** @type {!TestTabSearchApiProxy} */ /** @type {!TestTabSearchApiProxy} */
let testProxy; let testProxy;
disableScrollIntoViewAnimations(TabSearchItem);
/** /**
* @param {!NodeList<!Element>} rows * @param {!NodeList<!Element>} rows
* @param {!Array<number>} ids * @param {!Array<number>} ids
...@@ -37,8 +39,7 @@ suite('TabSearchAppTest', () => { ...@@ -37,8 +39,7 @@ suite('TabSearchAppTest', () => {
* @return {!NodeList<!Element>} * @return {!NodeList<!Element>}
*/ */
function queryRows() { function queryRows() {
return tabSearchApp.shadowRoot.querySelector('#tabsList') return tabSearchApp.shadowRoot.querySelectorAll('tab-search-item');
.shadowRoot.querySelectorAll('tab-search-item');
} }
/** /**
...@@ -101,8 +102,7 @@ suite('TabSearchAppTest', () => { ...@@ -101,8 +102,7 @@ suite('TabSearchAppTest', () => {
await setupTest({windows: [{active: true, tabs: [tabData]}]}); await setupTest({windows: [{active: true, tabs: [tabData]}]});
const tabSearchItem = /** @type {!HTMLElement} */ const tabSearchItem = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelector('tab-search-item'));
.shadowRoot.querySelector('tab-search-item'));
tabSearchItem.click(); tabSearchItem.click();
const [tabInfo] = await testProxy.whenCalled('switchToTab'); const [tabInfo] = await testProxy.whenCalled('switchToTab');
assertEquals(tabData.tabId, tabInfo.tabId); assertEquals(tabData.tabId, tabInfo.tabId);
...@@ -208,8 +208,7 @@ suite('TabSearchAppTest', () => { ...@@ -208,8 +208,7 @@ suite('TabSearchAppTest', () => {
await setupTest(sampleData()); await setupTest(sampleData());
verifyTabIds(queryRows(), [1, 5, 6, 2, 3, 4]); verifyTabIds(queryRows(), [1, 5, 6, 2, 3, 4]);
let tabSearchItem = /** @type {!HTMLElement} */ let tabSearchItem = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelector('tab-search-item[id="1"]'));
.shadowRoot.querySelector('tab-search-item[id="1"]'));
assertEquals('Google', tabSearchItem.data.tab.title); assertEquals('Google', tabSearchItem.data.tab.title);
assertEquals('https://www.google.com', tabSearchItem.data.tab.url); assertEquals('https://www.google.com', tabSearchItem.data.tab.url);
const updatedTab = /** @type {!Tab} */ ({ const updatedTab = /** @type {!Tab} */ ({
...@@ -224,8 +223,7 @@ suite('TabSearchAppTest', () => { ...@@ -224,8 +223,7 @@ suite('TabSearchAppTest', () => {
// tabIds are not changed after tab updated. // tabIds are not changed after tab updated.
verifyTabIds(queryRows(), [1, 5, 6, 2, 3, 4]); verifyTabIds(queryRows(), [1, 5, 6, 2, 3, 4]);
tabSearchItem = /** @type {!HTMLElement} */ tabSearchItem = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelector('tab-search-item[id="1"]'));
.shadowRoot.querySelector('tab-search-item[id="1"]'));
assertEquals(updatedTab.title, tabSearchItem.data.tab.title); assertEquals(updatedTab.title, tabSearchItem.data.tab.title);
assertEquals(updatedTab.url, tabSearchItem.data.tab.url); assertEquals(updatedTab.url, tabSearchItem.data.tab.url);
assertEquals('example.com', tabSearchItem.data.hostname); assertEquals('example.com', tabSearchItem.data.hostname);
...@@ -283,8 +281,7 @@ suite('TabSearchAppTest', () => { ...@@ -283,8 +281,7 @@ suite('TabSearchAppTest', () => {
// Click the first element with tabId 1. // Click the first element with tabId 1.
let tabSearchItem = /** @type {!HTMLElement} */ let tabSearchItem = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelector('tab-search-item[id="1"]'));
.shadowRoot.querySelector('tab-search-item[id="1"]'));
tabSearchItem.click(); tabSearchItem.click();
// Assert switchToTab() was called appropriately for an unfiltered tab list. // Assert switchToTab() was called appropriately for an unfiltered tab list.
...@@ -305,8 +302,7 @@ suite('TabSearchAppTest', () => { ...@@ -305,8 +302,7 @@ suite('TabSearchAppTest', () => {
testProxy.reset(); testProxy.reset();
// Click the only remaining element with tabId 2. // Click the only remaining element with tabId 2.
tabSearchItem = /** @type {!HTMLElement} */ tabSearchItem = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabsList') (tabSearchApp.shadowRoot.querySelector('tab-search-item[id="2"]'));
.shadowRoot.querySelector('tab-search-item[id="2"]'));
tabSearchItem.click(); tabSearchItem.click();
// Assert switchToTab() was called appropriately for a tab list fitlered by // Assert switchToTab() was called appropriately for a tab list fitlered by
...@@ -399,9 +395,8 @@ suite('TabSearchAppTest', () => { ...@@ -399,9 +395,8 @@ suite('TabSearchAppTest', () => {
const elements = [ const elements = [
tabSearchApp.shadowRoot.querySelector('#searchField'), tabSearchApp.shadowRoot.querySelector('#searchField'),
tabSearchApp.shadowRoot.querySelector('#tabsList'), tabSearchApp.shadowRoot.querySelector('#tabs'),
tabSearchApp.shadowRoot.querySelector('#tabsList') tabSearchApp.shadowRoot.querySelector('tab-search-item'),
.shadowRoot.querySelector('tab-search-item'),
tabSearchApp.shadowRoot.querySelector('#feedback-footer'), tabSearchApp.shadowRoot.querySelector('#feedback-footer'),
]; ];
...@@ -411,4 +406,39 @@ suite('TabSearchAppTest', () => { ...@@ -411,4 +406,39 @@ suite('TabSearchAppTest', () => {
assertEquals(4, testProxy.getCallCount('closeUI')); assertEquals(4, testProxy.getCallCount('closeUI'));
}); });
test('Scrollbar updates show previous and following list items', async () => {
await setupTest(generateSampleDataFromSiteNames(sampleSiteNames()));
const tabsDiv = /** @type {!HTMLElement} */
(tabSearchApp.shadowRoot.querySelector('#tabs'));
// Assert that the tabs are in a overflowing state.
assertGT(tabsDiv.scrollHeight, tabsDiv.clientHeight);
const tabItems = /** @type {!NodeList<!HTMLElement>}*/ (queryRows());
const searchField = /** @type {!TabSearchSearchField} */
(tabSearchApp.shadowRoot.querySelector('#searchField'));
for (let i = 0; i < tabItems.length; i++) {
keyDownOn(searchField, 0, [], 'ArrowDown');
const selectedIndex = ((i + 1) % tabItems.length);
assertEquals(selectedIndex, tabSearchApp.getSelectedIndex());
assertTabItemAndNeighborsInViewBounds(tabsDiv, tabItems, selectedIndex);
}
keyDownOn(searchField, 0, [], 'End');
assertTabItemInViewBounds(tabsDiv, tabItems[tabItems.length - 1]);
for (let i = tabItems.length - 1; i >= 0; i--) {
keyDownOn(searchField, 0, [], 'ArrowUp');
const selectedIndex = (i - 1 + tabItems.length) % tabItems.length;
assertEquals(selectedIndex, tabSearchApp.getSelectedIndex());
assertTabItemAndNeighborsInViewBounds(tabsDiv, tabItems, selectedIndex);
}
keyDownOn(searchField, 0, [], 'Home');
assertTabItemInViewBounds(tabsDiv, tabItems[0]);
});
}); });
...@@ -56,18 +56,6 @@ TEST_F('FuzzySearchTest', 'All', function() { ...@@ -56,18 +56,6 @@ TEST_F('FuzzySearchTest', 'All', function() {
mocha.run(); mocha.run();
}); });
// eslint-disable-next-line no-var
var InfiniteListTest = class extends TabSearchBrowserTest {
/** @override */
get browsePreload() {
return 'chrome://tab-search/test_loader.html?module=tab_search/infinite_list_test.js';
}
};
TEST_F('InfiniteListTest', 'All', function() {
mocha.run();
});
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var TabSearchItemTest = class extends TabSearchBrowserTest { var TabSearchItemTest = class extends TabSearchBrowserTest {
/** @override */ /** @override */
......
...@@ -57,9 +57,6 @@ export function sampleData() { ...@@ -57,9 +57,6 @@ export function sampleData() {
return profileTabs; return profileTabs;
} }
/**
* @return {!Array<string>}
*/
export function sampleSiteNames() { export function sampleSiteNames() {
return [ return [
'Google', 'Google',
...@@ -76,12 +73,11 @@ export function sampleSiteNames() { ...@@ -76,12 +73,11 @@ export function sampleSiteNames() {
} }
/** /**
* Generates sample tabs based on some given site names. * Generates profile data for a window with a series of tabs.
* @param {!Array} siteNames * @param {!Array} siteNames
* @return {!Array}
*/ */
export function generateSampleTabsFromSiteNames(siteNames) { export function generateSampleDataFromSiteNames(siteNames) {
return siteNames.map((siteName, i) => { const tabs = siteNames.map((siteName, i) => {
return { return {
index: i, index: i,
tabId: i + 1, tabId: i + 1,
...@@ -89,15 +85,6 @@ export function generateSampleTabsFromSiteNames(siteNames) { ...@@ -89,15 +85,6 @@ export function generateSampleTabsFromSiteNames(siteNames) {
url: 'https://www.' + siteName.toLowerCase() + '.com', url: 'https://www.' + siteName.toLowerCase() + '.com',
}; };
}); });
}
/** return {windows: [{active: true, tabs: tabs}]};
* Generates profile data for a window with a series of tabs.
* @param {!Array} siteNames
* @return {!Object}
*/
export function generateSampleDataFromSiteNames(siteNames) {
return {
windows: [{active: true, tabs: generateSampleTabsFromSiteNames(siteNames)}]
};
} }
...@@ -174,8 +174,7 @@ class TabSearchStoryTop100(TabSearchStory): ...@@ -174,8 +174,7 @@ class TabSearchStoryTop100(TabSearchStory):
SCROLL_ELEMENT_FUNCTION = ''' SCROLL_ELEMENT_FUNCTION = '''
document.querySelector('tab-search-app').shadowRoot.getElementById('tabsList') document.querySelector('tab-search-app').shadowRoot.querySelector('#tabs')
.shadowRoot.getElementById('container')
''' '''
MEASURE_FRAME_TIME_SCRIPT = ''' MEASURE_FRAME_TIME_SCRIPT = '''
......
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