Commit dace0dea authored by Esmael El-Moslimany's avatar Esmael El-Moslimany Committed by Commit Bot

WebUI NTP: most-visited/shortcuts, initial support

This implementation of most-visited is functionally the same of the
most-visited on the current NTP. The differences in UI are going to be
discussed with @bklmn after this CL lands.

 - Show up to 10 most-visited tiles.
 - Toast is shown with undo and restore to defaults buttons when
   certain tile actions are performed.
 - Action menu displays options to edit and remove tile.
 - Show add shortcut tile if space allows.
 - Supports light and dark mode.

Bug: 997229
Change-Id: Ie22220e2f866ba1ba485daf504265e9d868f9050
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1924702Reviewed-by: default avatarAlex Gough <ajgo@chromium.org>
Reviewed-by: default avatarDan Beam <dbeam@chromium.org>
Commit-Queue: Esmael Elmoslimany <aee@chromium.org>
Cr-Commit-Position: refs/heads/master@{#723050}
parent d7eaabe1
...@@ -8,8 +8,8 @@ import("//tools/polymer/polymer.gni") ...@@ -8,8 +8,8 @@ import("//tools/polymer/polymer.gni")
js_type_check("closure_compile") { js_type_check("closure_compile") {
is_polymer3 = true is_polymer3 = true
deps = [ deps = [
":app",
":browser_proxy", ":browser_proxy",
":manager",
] ]
} }
...@@ -22,22 +22,44 @@ js_library("browser_proxy") { ...@@ -22,22 +22,44 @@ js_library("browser_proxy") {
externs_list = [ "externs.js" ] externs_list = [ "externs.js" ]
} }
js_library("manager") { js_library("app") {
deps = [ deps = [
":browser_proxy", ":browser_proxy",
":most_visited",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled", "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
"//ui/webui/resources/cr_elements/cr_toast:cr_toast_manager.m",
]
}
js_library("most_visited") {
deps = [
":browser_proxy",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
"//ui/webui/resources/cr_elements/cr_action_menu:cr_action_menu.m",
"//ui/webui/resources/cr_elements/cr_dialog:cr_dialog.m",
"//ui/webui/resources/cr_elements/cr_toast:cr_toast_manager.m",
"//ui/webui/resources/js:assert.m", "//ui/webui/resources/js:assert.m",
"//ui/webui/resources/js:cr.m",
"//ui/webui/resources/js:load_time_data.m",
"//ui/webui/resources/js/cr/ui:focus_outline_manager.m",
] ]
} }
polymer_modulizer("manager") { polymer_modulizer("app") {
js_file = "manager.js" js_file = "app.js"
html_file = "manager.html" html_file = "app.html"
html_type = "v3-ready"
}
polymer_modulizer("most_visited") {
js_file = "most_visited.js"
html_file = "most_visited.html"
html_type = "v3-ready" html_type = "v3-ready"
} }
group("polymer3_elements") { group("polymer3_elements") {
deps = [ deps = [
":manager_module", ":app_module",
":most_visited_module",
] ]
} }
<style include="cr-shared-style"></style>
<most-visited></most-visited>
<cr-toast-manager duration="10000">
<cr-button id="undo" aria-label="$i18n{undoDescription}"
on-click="onUndoClick_">
$i18n{undo}
</cr-button>
<cr-button id="restore" aria-label="$i18n{restoreDefaultLinks}"
on-click="onRestoreDefaultsClick_">
$i18n{restoreDefaultLinks}
</cr-button>
</cr-toast-manager>
...@@ -2,28 +2,39 @@ ...@@ -2,28 +2,39 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import {assert} from 'chrome://resources/js/assert.m.js'; import './strings.m.js';
import './most_visited.js';
import 'chrome://resources/cr_elements/shared_style_css.m.js';
import {getToastManager} from 'chrome://resources/cr_elements/cr_toast/cr_toast_manager.m.js';
import {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 {BrowserProxy} from './browser_proxy.js'; import {BrowserProxy} from './browser_proxy.js';
class ManagerUI extends PolymerElement { class NewTabPageApp extends PolymerElement {
static get is() { static get is() {
return 'manager-ui'; return 'new-tab-page-app';
} }
static get template() { static get template() {
return html`{__html_template__}`; return html`{__html_template__}`;
} }
constructor() { get pageHandler_() {
super(); return BrowserProxy.getInstance().handler;
const browserProxy = BrowserProxy.getInstance(); }
/** @private {newTabPage.mojom.PageCallbackRouter} */
this.mojoEventTarget_ = browserProxy.callbackRouter; /** @private */
/** @private {newTabPage.mojom.PageHandlerInterface} */ onRestoreDefaultsClick_() {
this.mojoHandler_ = browserProxy.handler; getToastManager().hide();
this.pageHandler_.restoreMostVisitedDefaults();
}
/** @private */
onUndoClick_() {
getToastManager().hide();
this.pageHandler_.undoMostVisitedTileAction();
} }
} }
customElements.define(ManagerUI.is, ManagerUI); customElements.define(NewTabPageApp.is, NewTabPageApp);
...@@ -3,7 +3,10 @@ ...@@ -3,7 +3,10 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js'; import 'chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js';
import 'chrome://resources/mojo/url/mojom/url.mojom-lite.js';
import './new_tab_page.mojom-lite.js'; import './new_tab_page.mojom-lite.js';
import {addSingletonGetter} from 'chrome://resources/js/cr.m.js'; import {addSingletonGetter} from 'chrome://resources/js/cr.m.js';
export class BrowserProxy { export class BrowserProxy {
......
<style include="cr-hidden-style cr-icons">
:host {
--icon-size: 48px;
--tile-size: 112px;
--tile-margin: 16px;
--icon-background-color: rgb(229, 231, 232);
--icon-button-color: var(--google-grey-600);
--icon-button-color-active: var(--google-grey-refresh-700);
--tile-hover-color: rgba(var(--google-grey-900-rgb), .06);
--tile-title-color: var(--google-grey-800);
}
#container {
--content-width: calc(var(--column-count) * var(--tile-size)
/* We add an extra pixel because rounding errors on different zooms can
* make the width shorter than it should be. */
+ 1px);
display: flex;
flex-wrap: wrap;
height: calc(2 * (var(--tile-size) + var(--tile-margin)));
justify-content: center;
margin: 10px auto;
overflow: hidden;
transition: opacity 300ms ease-in-out;
width: calc(var(--content-width) + 12px);
}
.tile,
#addShortcut {
-webkit-tap-highlight-color: transparent;
align-items: center;
border-radius: 4px;
box-sizing: border-box;
cursor: pointer;
display: flex;
flex-direction: column;
height: var(--tile-size);
margin-bottom: var(--tile-margin);
opacity: 1;
padding-top: var(--tile-margin);
position: relative;
text-decoration: none;
transition-duration: 300ms;
transition-property: left, top;
transition-timing-function: ease-in-out;
user-select: none;
width: var(--tile-size);
}
:host-context(html:not(.focus-outline-visible)) .tile,
:host-context(html:not(.focus-outline-visible)) #addShortcut {
outline: none;
}
.tile:hover,
#addShortcut:hover {
background-color: var(--tile-hover-color);
}
.tile-icon {
align-items: center;
background-color: var(--icon-background-color);
border-radius: 50%;
display: flex;
height: var(--icon-size);
justify-content: center;
width: var(--icon-size);
}
.tile-title {
color: var(--tile-title-color);
direction: ltr;
font-weight: 500;
margin-top: 16px;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
width: 88px;
}
.tile.dragging {
background-color: var(--tile-hover-color);
transition-property: none;
z-index: 1;
}
cr-icon-button {
--cr-icon-button-fill-color: var(--icon-button-color);
--cr-icon-button-size: 20px;
margin: 4px 2px;
opacity: 0;
position: absolute;
right: 0;
top: 0;
transition-property: opacity;
}
:host-context([dir=rtl]) cr-icon-button {
left: 0;
right: unset;
}
.tile:hover cr-icon-button {
opacity: 1;
transition-delay: 500ms;
}
cr-icon-button:active,
cr-icon-button:focus,
cr-icon-button:hover {
--cr-icon-button-fill-color: var(--icon-button-color-active);
opacity: 1;
transition-delay: 0s;
}
@media (prefers-color-scheme: dark) {
:host {
--icon-background-color: var(--google-grey-refresh-100);
--icon-button-color: var(--google-grey-400);
--icon-button-color-active: var(--google-grey-200);
--tile-hover-color: rgba(255, 255, 255, .1);
--tile-title-color: white;
}
}
</style>
<div id="container" style="--column-count: [[columnCount_]]">
<dom-repeat id="tiles" items="[[tiles_]]">
<template>
<a class="tile" draggable="true" href$="[[item.url]]"
title$="[[item.title]]"
hidden$="[[isHidden_(index, columnCount_)]]">
<cr-icon-button class="icon-more-vert" on-click="onTileActionMenu_"
tabindex="0"></cr-icon-button>
<div class="tile-icon">
<img src$="[[getFaviconUrl_(item.url)]]" draggable="false"></img>
</div>
<div class="tile-title">[[item.title]]</div>
</a>
</template>
</dom-repeat>
<a id="addShortcut" tabindex="0" on-click="onAdd_" hidden$="[[!showAdd_]]"
title="$i18n{addLinkTitle}" on-keydown="onAddShortcutKeydown_">
<div class="tile-icon">
<img src="chrome://resources/images/add.svg" draggable="false"></img>
</div>
<div class="tile-title">$i18n{addLinkTitle}</div>
</a>
<cr-dialog id="dialog" on-close="onDialogClose_">
<div slot="title">[[dialogTitle_]]</div>
<div slot="body">
<cr-input id="dialogInputName" label="$i18n{nameField}"
value="{{dialogTileTitle_}}" autofocus spellcheck="false"></cr-input>
<cr-input id="dialogInputUrl" label="$i18n{urlField}"
value="{{dialogTileUrl_}}" invalid="[[dialogTileUrlInvalid_]]"
error-message="$i18n{invalidUrl}" spellcheck="false" type="url">
</cr-input>
</div>
<div slot="button-container">
<cr-button class="cancel-button" on-click="onDialogCancel_">
$i18n{linkCancel}
</cr-button>
<cr-button class="action-button" on-click="onSave_"
disabled$="[[!dialogTileUrl_]]">
$i18n{linkDone}
</cr-button>
</div>
</cr-dialog>
<cr-action-menu id="actionMenu">
<button id="actionMenuEdit" class="dropdown-item" on-click="onEdit_">
$i18n{editLinkTitle}
</button>
<button id="actionMenuRemove" class="dropdown-item" on-click="onRemove_">
$i18n{linkRemove}
</button>
</cr-action-menu>
</div>
// Copyright 2019 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 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.m.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.m.js';
import 'chrome://resources/cr_elements/cr_icons_css.m.js';
import 'chrome://resources/cr_elements/cr_input/cr_input.m.js';
import 'chrome://resources/cr_elements/hidden_style_css.m.js';
import './strings.m.js';
import {getToastManager} from 'chrome://resources/cr_elements/cr_toast/cr_toast_manager.m.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {isMac} from 'chrome://resources/js/cr.m.js';
import {FocusOutlineManager} from 'chrome://resources/js/cr/ui/focus_outline_manager.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {Debouncer, html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {BrowserProxy} from './browser_proxy.js';
/**
* @enum {number}
* @const
*/
const ScreenWidth = {
NARROW: 0,
MEDIUM: 1,
WIDE: 2,
};
/**
* @param {string} msgId
* @private
*/
function toast(msgId) {
getToastManager().show(loadTimeData.getString(msgId));
}
class MostVisited extends PolymerElement {
static get is() {
return 'most-visited';
}
static get template() {
return html`{__html_template__}`;
}
static get properties() {
return {
/** @private */
columnCount_: {
type: Boolean,
computed: 'computeColumnCount_(tiles_, screenWidth_)',
},
/** @private */
dialogTileTitle_: String,
/** @private */
dialogTileUrl_: String,
/** @private */
dialogTileUrlInvalid_: {
type: Boolean,
value: false,
},
/** @private */
dialogTitle_: String,
/** @private */
isRtl_: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
/** @private */
showAdd_: {
type: Boolean,
computed: 'computeShowAdd_(tiles_, columnCount_)',
},
/** @private {!ScreenWidth} */
screenWidth_: Number,
/** @private {!Array<!newTabPage.mojom.MostVisitedTile>} */
tiles_: Array,
};
}
constructor() {
super();
/** @private {boolean} */
this.adding_ = false;
const {callbackRouter, handler} = BrowserProxy.getInstance();
/** @private {!newTabPage.mojom.PageCallbackRouter} */
this.callbackRouter_ = callbackRouter;
/** @private {newTabPage.mojom.PageHandlerRemote} */
this.pageHandler_ = handler;
/** @private {?Debouncer} */
this.resizeDebouncer_ = null;
/** @private {?number} */
this.setMostVisitedTilesListenerId_ = null;
/** @private {number} */
this.targetIndex_ = -1;
}
/** @override */
connectedCallback() {
super.connectedCallback();
/** @private {boolean} */
this.isRtl_ = window.getComputedStyle(this)['direction'] === 'rtl';
this.setMostVisitedTilesListenerId_ =
this.callbackRouter_.setMostVisitedTiles.addListener(tiles => {
this.tiles_ = tiles.length > 10 ? tiles.slice(0, 10) : tiles;
});
FocusOutlineManager.forDocument(document);
}
/** @override */
disconnectedCallback() {
super.disconnectedCallback();
this.callbackRouter_.removeListener(
assert(this.setMostVisitedTilesListenerId_));
this.mediaListenerWideWidth_.removeListener(
assert(this.boundOnWidthChange_));
this.mediaListenerMediumWidth_.removeListener(
assert(this.boundOnWidthChange_));
}
/** @override */
ready() {
super.ready();
/** @private {!Function} */
this.boundOnWidthChange_ = this.updateScreenWidth_.bind(this);
/** @private {!MediaQueryList} */
this.mediaListenerWideWidth_ = window.matchMedia('(min-width: 672px)');
this.mediaListenerWideWidth_.addListener(this.boundOnWidthChange_);
/** @private {!MediaQueryList} */
this.mediaListenerMediumWidth_ = window.matchMedia('(min-width: 560px)');
this.mediaListenerMediumWidth_.addListener(this.boundOnWidthChange_);
this.updateScreenWidth_();
}
/**
* @return {number}
* @private
*/
computeColumnCount_() {
let maxColumns = 3;
if (this.screenWidth_ === ScreenWidth.WIDE) {
maxColumns = 5;
} else if (this.screenWidth_ === ScreenWidth.MEDIUM) {
maxColumns = 4;
}
// Add 1 for the add shortcut.
const tileCount = (this.tiles_ ? this.tiles_.length : 0) + 1;
const columnCount = tileCount <= maxColumns ?
tileCount :
Math.min(maxColumns, Math.ceil(tileCount / 2));
return columnCount;
}
/**
* @return {boolean}
* @private
*/
computeShowAdd_() {
return this.tiles_ && this.tiles_.length < this.columnCount_ * 2;
}
/**
* @param {!url.mojom.Url} url
* @return {string}
* @private
*/
getFaviconUrl_(url) {
const faviconUrl = new URL('chrome://favicon2/');
faviconUrl.searchParams.set('size', '24');
faviconUrl.searchParams.set('show_fallback_monogram', '');
faviconUrl.searchParams.set('page_url', url.url);
return faviconUrl.href;
}
/**
* @param {number} index
* @return {boolean}
* @private
*/
isHidden_(index) {
return index >= this.columnCount_ * 2;
}
/** @private */
onAdd_() {
this.dialogTitle_ = loadTimeData.getString('addLinkTitle');
this.dialogTileTitle_ = '';
this.dialogTileUrl_ = '';
this.dialogTileUrlInvalid_ = false;
this.adding_ = true;
this.$.dialog.showModal();
}
/**
* @param {!KeyboardEvent} e
* @private
*/
onAddShortcutKeydown_(e) {
if (e.altKey || e.shiftKey || e.metaKey || e.ctrlKey) {
return;
}
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.onAdd_();
}
}
/** @private */
onDialogCancel_() {
this.targetIndex_ = -1;
this.$.dialog.cancel();
}
/** @private */
onDialogClose_() {
if (this.adding_) {
this.$.addShortcut.focus();
}
this.adding_ = false;
}
/** @private */
onEdit_() {
this.$.actionMenu.close();
this.dialogTitle_ = loadTimeData.getString('editLinkTitle');
const {title, url} = this.tiles_[this.targetIndex_];
this.dialogTileTitle_ = title;
this.dialogTileUrl_ = url.url;
this.dialogTileUrlInvalid_ = false;
this.$.dialog.showModal();
}
/** @private */
async onRemove_() {
this.$.actionMenu.close();
const {title, url} = this.tiles_[this.targetIndex_];
const {success} = await this.pageHandler_.deleteMostVisitedTile(url);
toast(success ? 'linkRemove' : 'linkCantRemove');
const focusIndex =
Math.min(this.tiles_.length, Math.max(0, this.targetIndex_));
this.shadowRoot.querySelectorAll('.tile, #addShortcut')[focusIndex].focus();
this.targetIndex_ = -1;
}
/** @private */
async onSave_() {
let newUrl;
try {
newUrl = new URL(
this.dialogTileUrl_.includes('://') ?
this.dialogTileUrl_ :
`https://${this.dialogTileUrl_}/`);
if (!['http:', 'https:'].includes(newUrl.protocol)) {
throw new Error();
}
} catch (e) {
this.dialogTileUrlInvalid_ = true;
return;
}
this.dialogTileUrlInvalid_ = false;
this.$.dialog.close();
let newTitle = this.dialogTileTitle_.trim();
if (newTitle.length === 0) {
newTitle = this.dialogTileUrl_;
}
if (this.adding_) {
const {success} = await this.pageHandler_.addMostVisitedTile(
{url: newUrl.href}, newTitle);
toast(success ? 'linkAddedMsg' : 'linkCantCreate');
} else {
const {url, title} = this.tiles_[this.targetIndex_];
if (url.url !== newUrl.href || title !== newTitle) {
const {success} = await this.pageHandler_.updateMostVisitedTile(
url, {url: newUrl.href}, newTitle);
toast(success ? 'linkEditedMsg' : 'linkCantEdit');
}
this.targetIndex_ = -1;
}
}
/**
* @param {!Event} e
* @private
*/
onTileActionMenu_(e) {
e.preventDefault();
this.targetIndex_ =
this.$.tiles.modelForElement(e.target.parentElement).index;
this.$.actionMenu.showAt(e.target);
}
/** @private */
updateScreenWidth_() {
if (this.mediaListenerWideWidth_.matches) {
this.screenWidth_ = ScreenWidth.WIDE;
} else if (this.mediaListenerMediumWidth_.matches) {
this.screenWidth_ = ScreenWidth.MEDIUM;
} else {
this.screenWidth_ = ScreenWidth.NARROW;
}
}
}
customElements.define(MostVisited.is, MostVisited);
...@@ -11,12 +11,20 @@ ...@@ -11,12 +11,20 @@
} }
body { body {
background: white;
margin: 0; margin: 0;
} }
@media (prefers-color-scheme: dark) {
body {
background: rgb(67, 67, 71);
}
}
</style> </style>
</head> </head>
<body> <body>
<manager-ui></manager-ui> <new-tab-page-app></new-tab-page-app>
<script type="module" src="manager.js"></script> <script type="module" src="app.js"></script>
<link rel="stylesheet" href="chrome://resources/css/text_defaults_md.css">
</body> </body>
</html> </html>
\ No newline at end of file
...@@ -15,8 +15,11 @@ ...@@ -15,8 +15,11 @@
<include name="IDR_NEW_TAB_PAGE_MOJO_LITE_JS" <include name="IDR_NEW_TAB_PAGE_MOJO_LITE_JS"
file="${root_gen_dir}/chrome/browser/ui/webui/new_tab_page/new_tab_page.mojom-lite.js" file="${root_gen_dir}/chrome/browser/ui/webui/new_tab_page/new_tab_page.mojom-lite.js"
use_base_dir="false" type="BINDATA" compress="gzip" /> use_base_dir="false" type="BINDATA" compress="gzip" />
<include name="IDR_NEW_TAB_PAGE_MANAGER_JS" <include name="IDR_NEW_TAB_PAGE_APP_JS"
file="${root_gen_dir}/chrome/browser/resources/new_tab_page/manager.js" file="${root_gen_dir}/chrome/browser/resources/new_tab_page/app.js"
use_base_dir="false" type="BINDATA" compress="gzip" />
<include name="IDR_NEW_TAB_PAGE_MOST_VISITED_JS"
file="${root_gen_dir}/chrome/browser/resources/new_tab_page/most_visited.js"
use_base_dir="false" type="BINDATA" compress="gzip" /> use_base_dir="false" type="BINDATA" compress="gzip" />
</includes> </includes>
<structures> <structures>
......
...@@ -8,4 +8,8 @@ mojom("mojo_bindings") { ...@@ -8,4 +8,8 @@ mojom("mojo_bindings") {
sources = [ sources = [
"new_tab_page.mojom", "new_tab_page.mojom",
] ]
public_deps = [
"//url/mojom:url_mojom_gurl",
]
} }
...@@ -4,6 +4,13 @@ ...@@ -4,6 +4,13 @@
module new_tab_page.mojom; module new_tab_page.mojom;
import "url/mojom/url.mojom";
struct MostVisitedTile {
string title;
url.mojom.Url url;
};
// Used by the WebUI page to bootstrap bidirectional communication. // Used by the WebUI page to bootstrap bidirectional communication.
interface PageHandlerFactory { interface PageHandlerFactory {
// The WebUI page's |BrowserProxy| singleton calls this method when the page // The WebUI page's |BrowserProxy| singleton calls this method when the page
...@@ -14,8 +21,23 @@ interface PageHandlerFactory { ...@@ -14,8 +21,23 @@ interface PageHandlerFactory {
// Browser-side handler for requests from WebUI page. // Browser-side handler for requests from WebUI page.
interface PageHandler { interface PageHandler {
// Adds tile.
AddMostVisitedTile(url.mojom.Url url, string title) => (bool success);
// Deletes tile by |url|.
DeleteMostVisitedTile(url.mojom.Url url) => (bool success);
// Replaces the custom and most-visited tiles with the default tile set.
RestoreMostVisitedDefaults();
// Undoes the last action done to the tiles (add, delete, reorder, restore or
// update). Note that only the last action can be undone.
UndoMostVisitedTileAction();
// Updates a tile by url.
UpdateMostVisitedTile(url.mojom.Url url, url.mojom.Url new_url,
string new_title)
=> (bool success);
}; };
// WebUI-side handler for requests from the browser. // WebUI-side handler for requests from the browser.
interface Page { interface Page {
// Updates the page with tiles.
SetMostVisitedTiles(array<MostVisitedTile> tiles);
}; };
...@@ -3,12 +3,73 @@ ...@@ -3,12 +3,73 @@
// found in the LICENSE file. // found in the LICENSE file.
#include "chrome/browser/ui/webui/new_tab_page/new_tab_page_handler.h" #include "chrome/browser/ui/webui/new_tab_page/new_tab_page_handler.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ntp_tiles/chrome_most_visited_sites_factory.h"
#include "chrome/browser/profiles/profile.h"
NewTabPageHandler::NewTabPageHandler( NewTabPageHandler::NewTabPageHandler(
mojo::PendingReceiver<new_tab_page::mojom::PageHandler> mojo::PendingReceiver<new_tab_page::mojom::PageHandler>
pending_page_handler, pending_page_handler,
mojo::PendingRemote<new_tab_page::mojom::Page> pending_page) mojo::PendingRemote<new_tab_page::mojom::Page> pending_page,
Profile* profile)
: page_{std::move(pending_page)}, : page_{std::move(pending_page)},
receiver_{this, std::move(pending_page_handler)} {} receiver_{this, std::move(pending_page_handler)} {
most_visited_sites_ = ChromeMostVisitedSitesFactory::NewForProfile(profile);
// 9 tiles are required for the custom links feature in order to balance the
// Most Visited rows (this is due to an additional "Add" button).
most_visited_sites_->SetMostVisitedURLsObserver(this, 9);
most_visited_sites_->EnableCustomLinks(true);
}
NewTabPageHandler::~NewTabPageHandler() = default; NewTabPageHandler::~NewTabPageHandler() = default;
void NewTabPageHandler::AddMostVisitedTile(
const GURL& url,
const std::string& title,
AddMostVisitedTileCallback callback) {
bool success =
most_visited_sites_->AddCustomLink(url, base::UTF8ToUTF16(title));
std::move(callback).Run(success);
}
void NewTabPageHandler::DeleteMostVisitedTile(
const GURL& url,
DeleteMostVisitedTileCallback callback) {
bool success = most_visited_sites_->DeleteCustomLink(url);
std::move(callback).Run(success);
}
void NewTabPageHandler::RestoreMostVisitedDefaults() {
most_visited_sites_->UninitializeCustomLinks();
}
void NewTabPageHandler::UpdateMostVisitedTile(
const GURL& url,
const GURL& new_url,
const std::string& new_title,
UpdateMostVisitedTileCallback callback) {
bool success = most_visited_sites_->UpdateCustomLink(
url, new_url != url ? new_url : GURL(), base::UTF8ToUTF16(new_title));
std::move(callback).Run(success);
}
void NewTabPageHandler::UndoMostVisitedTileAction() {
most_visited_sites_->UndoCustomLinkAction();
}
void NewTabPageHandler::OnURLsAvailable(
const std::map<ntp_tiles::SectionType, ntp_tiles::NTPTilesVector>&
sections) {
DCHECK(most_visited_sites_);
std::vector<new_tab_page::mojom::MostVisitedTilePtr> list;
// Use only personalized tiles for instant service.
const ntp_tiles::NTPTilesVector& tiles =
sections.at(ntp_tiles::SectionType::PERSONALIZED);
for (const ntp_tiles::NTPTile& tile : tiles) {
auto value = new_tab_page::mojom::MostVisitedTile::New();
value->title = base::UTF16ToUTF8(tile.title);
value->url = tile.url;
list.push_back(std::move(value));
}
page_->SetMostVisitedTiles(std::move(list));
}
...@@ -7,22 +7,50 @@ ...@@ -7,22 +7,50 @@
#include "base/macros.h" #include "base/macros.h"
#include "chrome/browser/ui/webui/new_tab_page/new_tab_page.mojom.h" #include "chrome/browser/ui/webui/new_tab_page/new_tab_page.mojom.h"
#include "components/ntp_tiles/most_visited_sites.h"
#include "components/ntp_tiles/ntp_tile.h"
#include "content/public/browser/web_contents_observer.h" #include "content/public/browser/web_contents_observer.h"
#include "mojo/public/cpp/bindings/pending_receiver.h" #include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h" #include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h" #include "mojo/public/cpp/bindings/remote.h"
class GURL;
class Profile;
class NewTabPageHandler : public content::WebContentsObserver, class NewTabPageHandler : public content::WebContentsObserver,
public ntp_tiles::MostVisitedSites::Observer,
public new_tab_page::mojom::PageHandler { public new_tab_page::mojom::PageHandler {
public: public:
NewTabPageHandler( NewTabPageHandler(mojo::PendingReceiver<new_tab_page::mojom::PageHandler>
mojo::PendingReceiver<new_tab_page::mojom::PageHandler> pending_page_handler,
pending_page_handler, mojo::PendingRemote<new_tab_page::mojom::Page> pending_page,
mojo::PendingRemote<new_tab_page::mojom::Page> pending_page); Profile* profile);
~NewTabPageHandler() override; ~NewTabPageHandler() override;
// new_tab_page::mojom::PageHandler:
void AddMostVisitedTile(const GURL& url,
const std::string& title,
AddMostVisitedTileCallback callback) override;
void DeleteMostVisitedTile(const GURL& url,
DeleteMostVisitedTileCallback callback) override;
void RestoreMostVisitedDefaults() override;
void UpdateMostVisitedTile(const GURL& url,
const GURL& new_url,
const std::string& new_title,
UpdateMostVisitedTileCallback callback) override;
void UndoMostVisitedTileAction() override;
private: private:
// ntp_tiles::MostVisitedSites::Observer implementation.
void OnURLsAvailable(
const std::map<ntp_tiles::SectionType, ntp_tiles::NTPTilesVector>&
sections) override;
void OnIconMadeAvailable(const GURL& site_url) override {}
// Data source for NTP tiles (aka Most Visited tiles). May be null.
std::unique_ptr<ntp_tiles::MostVisitedSites> most_visited_sites_;
mojo::Remote<new_tab_page::mojom::Page> page_; mojo::Remote<new_tab_page::mojom::Page> page_;
mojo::Receiver<new_tab_page::mojom::PageHandler> receiver_; mojo::Receiver<new_tab_page::mojom::PageHandler> receiver_;
......
...@@ -5,45 +5,65 @@ ...@@ -5,45 +5,65 @@
#include "chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.h" #include "chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.h"
#include "chrome/browser/profiles/profile.h" #include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/webui/managed_ui_handler.h" #include "chrome/browser/ui/webui/favicon_source.h"
#include "chrome/browser/ui/webui/new_tab_page/new_tab_page_handler.h" #include "chrome/browser/ui/webui/new_tab_page/new_tab_page_handler.h"
#include "chrome/browser/ui/webui/webui_util.h" #include "chrome/browser/ui/webui/webui_util.h"
#include "chrome/common/url_constants.h" #include "chrome/common/url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/new_tab_page_resources.h" #include "chrome/grit/new_tab_page_resources.h"
#include "chrome/grit/new_tab_page_resources_map.h" #include "chrome/grit/new_tab_page_resources_map.h"
#include "components/favicon_base/favicon_url_parser.h"
#include "components/strings/grit/components_strings.h" #include "components/strings/grit/components_strings.h"
#include "content/public/browser/url_data_source.h" #include "content/public/browser/url_data_source.h"
#include "content/public/browser/web_ui_data_source.h" #include "content/public/browser/web_ui_data_source.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/l10n/l10n_util.h"
using content::BrowserContext; using content::BrowserContext;
using content::WebContents; using content::WebContents;
namespace { namespace {
constexpr char kGeneratedPath[] =
"@out_folder@/gen/chrome/browser/resources/new_tab_page/";
content::WebUIDataSource* CreateNewTabPageUiHtmlSource() { content::WebUIDataSource* CreateNewTabPageUiHtmlSource() {
content::WebUIDataSource* source = content::WebUIDataSource* source =
content::WebUIDataSource::Create(chrome::kChromeUINewTabPageHost); content::WebUIDataSource::Create(chrome::kChromeUINewTabPageHost);
ui::Accelerator undo_accelerator(ui::VKEY_Z, ui::EF_PLATFORM_ACCELERATOR);
source->AddString("undoDescription", l10n_util::GetStringFUTF16(
IDS_UNDO_DESCRIPTION,
undo_accelerator.GetShortcutText()));
static constexpr webui::LocalizedString kStrings[] = { static constexpr webui::LocalizedString kStrings[] = {
{"title", IDS_NEW_TAB_TITLE}, {"title", IDS_NEW_TAB_TITLE},
{"undo", IDS_NEW_TAB_UNDO_THUMBNAIL_REMOVE},
// Custom Links
{"addLinkTitle", IDS_NTP_CUSTOM_LINKS_ADD_SHORTCUT_TITLE},
{"editLinkTitle", IDS_NTP_CUSTOM_LINKS_EDIT_SHORTCUT},
{"invalidUrl", IDS_NTP_CUSTOM_LINKS_INVALID_URL},
{"linkAddedMsg", IDS_NTP_CONFIRM_MSG_SHORTCUT_ADDED},
{"linkCancel", IDS_NTP_CUSTOM_LINKS_CANCEL},
{"linkCantCreate", IDS_NTP_CUSTOM_LINKS_CANT_CREATE},
{"linkCantEdit", IDS_NTP_CUSTOM_LINKS_CANT_EDIT},
{"linkCantRemove", IDS_NTP_CUSTOM_LINKS_CANT_REMOVE},
{"linkDone", IDS_NTP_CUSTOM_LINKS_DONE},
{"linkEditedMsg", IDS_NTP_CONFIRM_MSG_SHORTCUT_EDITED},
{"linkRemove", IDS_NTP_CUSTOM_LINKS_REMOVE},
{"linkRemovedMsg", IDS_NTP_CONFIRM_MSG_SHORTCUT_REMOVED},
{"nameField", IDS_NTP_CUSTOM_LINKS_NAME},
{"restoreDefaultLinks", IDS_NTP_CONFIRM_MSG_RESTORE_DEFAULTS},
{"urlField", IDS_NTP_CUSTOM_LINKS_URL},
}; };
AddLocalizedStringsBulk(source, kStrings); AddLocalizedStringsBulk(source, kStrings);
source->AddResourcePath("new_tab_page.mojom-lite.js", source->AddResourcePath("new_tab_page.mojom-lite.js",
IDR_NEW_TAB_PAGE_MOJO_LITE_JS); IDR_NEW_TAB_PAGE_MOJO_LITE_JS);
webui::SetupWebUIDataSource(
std::string generated_path = source, base::make_span(kNewTabPageResources, kNewTabPageResourcesSize),
"@out_folder@/gen/chrome/browser/resources/new_tab_page/"; kGeneratedPath, IDR_NEW_TAB_PAGE_NEW_TAB_PAGE_HTML);
for (size_t i = 0; i < kNewTabPageResourcesSize; ++i) {
base::StringPiece path = kNewTabPageResources[i].name;
if (path.rfind(generated_path, 0) == 0) {
path = path.substr(generated_path.length());
}
source->AddResourcePath(path, kNewTabPageResources[i].value);
}
source->SetDefaultResource(IDR_NEW_TAB_PAGE_NEW_TAB_PAGE_HTML);
source->UseStringsJs();
return source; return source;
} }
...@@ -52,11 +72,13 @@ content::WebUIDataSource* CreateNewTabPageUiHtmlSource() { ...@@ -52,11 +72,13 @@ content::WebUIDataSource* CreateNewTabPageUiHtmlSource() {
NewTabPageUI::NewTabPageUI(content::WebUI* web_ui) NewTabPageUI::NewTabPageUI(content::WebUI* web_ui)
: ui::MojoWebUIController(web_ui, true), page_factory_receiver_(this) { : ui::MojoWebUIController(web_ui, true), page_factory_receiver_(this) {
Profile* profile = Profile::FromWebUI(web_ui); profile_ = Profile::FromWebUI(web_ui);
content::WebUIDataSource::Add(profile_, CreateNewTabPageUiHtmlSource());
content::WebUIDataSource* source = CreateNewTabPageUiHtmlSource(); content::URLDataSource::Add(
ManagedUIHandler::Initialize(web_ui, source); profile_, std::make_unique<FaviconSource>(
content::WebUIDataSource::Add(profile, source); profile_, chrome::FaviconUrlFormat::kFavicon2));
AddHandlerToRegistry(base::BindRepeating( AddHandlerToRegistry(base::BindRepeating(
&NewTabPageUI::BindPageHandlerFactory, base::Unretained(this))); &NewTabPageUI::BindPageHandlerFactory, base::Unretained(this)));
...@@ -80,7 +102,7 @@ void NewTabPageUI::CreatePageHandler( ...@@ -80,7 +102,7 @@ void NewTabPageUI::CreatePageHandler(
pending_page_handler) { pending_page_handler) {
DCHECK(pending_page.is_valid()); DCHECK(pending_page.is_valid());
page_handler_ = std::make_unique<NewTabPageHandler>( page_handler_ = std::make_unique<NewTabPageHandler>(
std::move(pending_page_handler), std::move(pending_page)); std::move(pending_page_handler), std::move(pending_page), profile_);
} }
// static // static
......
...@@ -17,6 +17,7 @@ class WebUI; ...@@ -17,6 +17,7 @@ class WebUI;
} }
class GURL; class GURL;
class NewTabPageHandler; class NewTabPageHandler;
class Profile;
class NewTabPageUI : public ui::MojoWebUIController, class NewTabPageUI : public ui::MojoWebUIController,
public new_tab_page::mojom::PageHandlerFactory { public new_tab_page::mojom::PageHandlerFactory {
...@@ -42,6 +43,8 @@ class NewTabPageUI : public ui::MojoWebUIController, ...@@ -42,6 +43,8 @@ class NewTabPageUI : public ui::MojoWebUIController,
mojo::Receiver<new_tab_page::mojom::PageHandlerFactory> mojo::Receiver<new_tab_page::mojom::PageHandlerFactory>
page_factory_receiver_; page_factory_receiver_;
Profile* profile_;
DISALLOW_COPY_AND_ASSIGN(NewTabPageUI); DISALLOW_COPY_AND_ASSIGN(NewTabPageUI);
}; };
......
...@@ -223,6 +223,7 @@ js2gtest("browser_tests_js_mojo_lite_webui") { ...@@ -223,6 +223,7 @@ js2gtest("browser_tests_js_mojo_lite_webui") {
"engagement/site_engagement_browsertest.js", "engagement/site_engagement_browsertest.js",
"interventions_internals_browsertest.js", "interventions_internals_browsertest.js",
"media/media_engagement_browsertest.js", "media/media_engagement_browsertest.js",
"new_tab_page/new_tab_page_browsertest.js",
"usb_internals_browsertest.js", "usb_internals_browsertest.js",
] ]
......
// Copyright 2019 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 'chrome://new-tab-page/app.js';
import {BrowserProxy} from 'chrome://new-tab-page/browser_proxy.js';
import {TestProxy} from 'chrome://test/new_tab_page/test_support.js';
import {eventToPromise, flushTasks} from 'chrome://test/test_util.m.js';
suite('NewTabPageAppTest', () => {
/** @type {!NewTabPageApp} */
let app;
/** @type {TestProxy} */
let testProxy;
/** @type {CrToastManagerElement} */
let toastManager;
setup(() => {
PolymerTest.clearBody();
testProxy = new TestProxy();
BrowserProxy.instance_ = testProxy;
app = document.createElement('new-tab-page-app');
document.body.appendChild(app);
toastManager = app.shadowRoot.querySelector('cr-toast-manager');
});
test('toast restore defaults button', async () => {
const wait = testProxy.handler.whenCalled('restoreMostVisitedDefaults');
assertFalse(toastManager.isToastOpen);
toastManager.show('');
assertTrue(toastManager.isToastOpen);
app.$.restore.click();
await wait;
assertFalse(toastManager.isToastOpen);
});
test('toast undo button', async () => {
const wait = testProxy.handler.whenCalled('undoMostVisitedTileAction');
assertFalse(toastManager.isToastOpen);
toastManager.show('');
assertTrue(toastManager.isToastOpen);
app.$.undo.click();
await wait;
assertFalse(toastManager.isToastOpen);
});
});
// Copyright 2019 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 'chrome://new-tab-page/most_visited.js';
import {BrowserProxy} from 'chrome://new-tab-page/browser_proxy.js';
import {TestProxy} from 'chrome://test/new_tab_page/test_support.js';
import {eventToPromise, flushTasks} from 'chrome://test/test_util.m.js';
suite('NewTabPageMostVisitedTest', () => {
/** @type {!MostVisited} */
let mostVisited;
/** @type {TestProxy} */
let testProxy;
/** @type {CrToastManagerElement} */
let toastManager;
/**
* @param {string}
* @return {!Array<!HTMLElement>}
* @private
*/
function queryAll(q) {
return Array.from(mostVisited.shadowRoot.querySelectorAll(q));
}
/**
* @return {!Array<!HTMLElement>}
* @private
*/
function queryTiles() {
return queryAll('.tile');
}
/**
* @param {number} n
* @return {!Promise}
* @private
*/
async function addTiles(n) {
const tiles = Array(n).fill(0).map((x, i) => {
const char = String.fromCharCode(i + /* 'a' */ 97);
return {title: char, url: {url: `https://${char}/`}};
});
const tilesRendered = eventToPromise('dom-change', mostVisited.$.tiles);
testProxy.callbackRouterRemote.setMostVisitedTiles(tiles);
await testProxy.callbackRouterRemote.$.flushForTesting();
await tilesRendered;
}
setup(() => {
PolymerTest.clearBody();
testProxy = new TestProxy();
BrowserProxy.instance_ = testProxy;
mostVisited = document.createElement('most-visited');
document.body.appendChild(mostVisited);
toastManager = document.createElement('cr-toast-manager');
document.body.appendChild(toastManager);
});
test('empty shows add shortcut only', () => {
assertEquals(0, queryTiles().length);
assertFalse(mostVisited.$.addShortcut.hidden);
});
test('clicking on add shortcut opens dialog', () => {
assertFalse(mostVisited.$.dialog.open);
mostVisited.$.addShortcut.click();
assertTrue(mostVisited.$.dialog.open);
});
test('pressing enter when add shortcut has focus opens dialog', () => {
mostVisited.$.addShortcut.focus();
assertFalse(mostVisited.$.dialog.open);
mostVisited.$.addShortcut.dispatchEvent(
new KeyboardEvent('keydown', {key: 'Enter'}));
assertTrue(mostVisited.$.dialog.open);
});
test('pressing space when add shortcut has focus opens dialog', () => {
mostVisited.$.addShortcut.focus();
assertFalse(mostVisited.$.dialog.open);
mostVisited.$.addShortcut.dispatchEvent(
new KeyboardEvent('keydown', {key: ' '}));
assertTrue(mostVisited.$.dialog.open);
});
test('four tiles fit on one line with addShortcut', async () => {
await addTiles(4);
assertEquals(4, queryTiles().length);
assertFalse(mostVisited.$.addShortcut.hidden);
const tops = queryAll('a').map(({offsetTop}) => offsetTop);
assertEquals(5, tops.length);
tops.forEach(top => {
assertEquals(tops[0], top);
});
});
test('five tiles are displayed on two rows with addShortcut', async () => {
await addTiles(5);
assertEquals(5, queryTiles().length);
assertFalse(mostVisited.$.addShortcut.hidden);
const tops = queryAll('a').map(({offsetTop}) => offsetTop);
assertEquals(6, tops.length);
const firstRowTop = tops[0];
const secondRowTop = tops[3];
assertNotEquals(firstRowTop, secondRowTop);
tops.slice(0, 3).forEach(top => {
assertEquals(firstRowTop, top);
});
tops.slice(3).forEach(top => {
assertEquals(secondRowTop, top);
});
});
test('nine tiles are displayed on two rows with addShortcut', async () => {
await addTiles(9);
assertEquals(9, queryTiles().length);
assertFalse(mostVisited.$.addShortcut.hidden);
const tops = queryAll('a').map(({offsetTop}) => offsetTop);
assertEquals(10, tops.length);
const firstRowTop = tops[0];
const secondRowTop = tops[5];
assertNotEquals(firstRowTop, secondRowTop);
tops.slice(0, 5).forEach(top => {
assertEquals(firstRowTop, top);
});
tops.slice(5).forEach(top => {
assertEquals(secondRowTop, top);
});
});
test('ten tiles are displayed on two rows without addShortcut', async () => {
await addTiles(10);
assertEquals(10, queryTiles().length);
assertTrue(mostVisited.$.addShortcut.hidden);
const tops = queryAll('a:not([hidden])').map(a => a.offsetTop);
assertEquals(10, tops.length);
const firstRowTop = tops[0];
const secondRowTop = tops[5];
assertNotEquals(firstRowTop, secondRowTop);
tops.slice(0, 5).forEach(top => {
assertEquals(firstRowTop, top);
});
tops.slice(5).forEach(top => {
assertEquals(secondRowTop, top);
});
});
test('ten tiles is the max tiles displayed', async () => {
await addTiles(11);
assertEquals(10, queryTiles().length);
assertTrue(mostVisited.$.addShortcut.hidden);
});
test('dialog opens when add shortcut clicked', () => {
const {dialog} = mostVisited.$;
assertFalse(dialog.open);
mostVisited.$.addShortcut.click();
assertTrue(dialog.open);
});
suite('add dialog', () => {
/** @private {CrDialogElement} */
let dialog;
/** @private {CrInputElement} */
let inputName;
/** @private {CrInputElement} */
let inputUrl;
/** @private {CrButtonElement} */
let saveButton;
/** @private {CrButtonElement} */
let cancelButton;
setup(() => {
dialog = mostVisited.$.dialog;
inputName = mostVisited.$.dialogInputName;
inputUrl = mostVisited.$.dialogInputUrl;
saveButton = dialog.querySelector('.action-button');
cancelButton = dialog.querySelector('.cancel-button');
mostVisited.$.addShortcut.click();
});
test('inputs are initially empty', () => {
assertEquals('', inputName.value);
assertEquals('', inputUrl.value);
});
test('saveButton is enabled with URL is not empty', () => {
assertTrue(saveButton.disabled);
inputName.value = 'name';
assertTrue(saveButton.disabled);
inputUrl.value = 'url';
assertFalse(saveButton.disabled);
inputUrl.value = '';
assertTrue(saveButton.disabled);
});
test('cancel closes dialog', () => {
assertTrue(dialog.open);
cancelButton.click();
assertFalse(dialog.open);
});
test('inputs are clear after dialog reuse', () => {
inputName.value = 'name';
inputUrl.value = 'url';
cancelButton.click();
mostVisited.$.addShortcut.click();
assertEquals('', inputName.value);
assertEquals('', inputUrl.value);
});
test('use URL input for title when title empty', async () => {
inputUrl.value = 'url';
const addCalled = testProxy.handler.whenCalled('addMostVisitedTile');
saveButton.click();
const [url, title] = await addCalled;
assertEquals('url', title);
});
test('toast shown on save', async () => {
inputUrl.value = 'url';
assertFalse(toastManager.isToastOpen);
const addCalled = testProxy.handler.whenCalled('addMostVisitedTile');
saveButton.click();
await addCalled;
assertTrue(toastManager.isToastOpen);
});
test('save name and URL', async () => {
inputName.value = 'name';
inputUrl.value = 'https://url/';
const addCalled = testProxy.handler.whenCalled('addMostVisitedTile');
saveButton.click();
const [{url}, title] = await addCalled;
assertEquals('name', title);
assertEquals('https://url/', url);
});
test('dialog closes on save', () => {
inputUrl.value = 'url';
assertTrue(dialog.open);
saveButton.click();
assertFalse(dialog.open);
});
test('https:// is added if no scheme is used', async () => {
inputUrl.value = 'url';
const addCalled = testProxy.handler.whenCalled('addMostVisitedTile');
saveButton.click();
const [{url}, title] = await addCalled;
assertEquals('https://url/', url);
});
test('http is a valid scheme', async () => {
inputUrl.value = 'http://url';
const addCalled = testProxy.handler.whenCalled('addMostVisitedTile');
saveButton.click();
await addCalled;
});
test('https is a valid scheme', async () => {
inputUrl.value = 'https://url';
const addCalled = testProxy.handler.whenCalled('addMostVisitedTile');
saveButton.click();
await addCalled;
});
test('chrome is not a valid scheme', () => {
inputUrl.value = 'chrome://url';
assertFalse(inputUrl.invalid);
saveButton.click();
assertTrue(inputUrl.invalid);
});
});
test('open edit dialog', async () => {
await addTiles(2);
const {actionMenu, dialog} = mostVisited.$;
assertFalse(actionMenu.open);
queryTiles()[0].querySelector('cr-icon-button').click();
assertTrue(actionMenu.open);
assertFalse(dialog.open);
mostVisited.$.actionMenuEdit.click();
assertFalse(actionMenu.open);
assertTrue(dialog.open);
});
suite('edit dialog', () => {
/** @private {CrActionMenuElement} */
let actionMenu;
/** @private {CrIconButtonElement} */
let actionMenuButton;
/** @private {CrDialogElement} */
let dialog;
/** @private {CrInputElement} */
let inputName;
/** @private {CrInputElement} */
let inputUrl;
/** @private {CrButtonElement} */
let saveButton;
/** @private {CrButtonElement} */
let cancelButton;
/** @private {HTMLElement} */
let tile;
setup(async () => {
actionMenu = mostVisited.$.actionMenu;
dialog = mostVisited.$.dialog;
inputName = mostVisited.$.dialogInputName;
inputUrl = mostVisited.$.dialogInputUrl;
saveButton = dialog.querySelector('.action-button');
cancelButton = dialog.querySelector('.cancel-button');
await addTiles(2);
tile = queryTiles()[1];
actionMenuButton = tile.querySelector('cr-icon-button');
actionMenuButton.click();
mostVisited.$.actionMenuEdit.click();
});
test('edit a tile URL', async () => {
assertEquals('https://b/', inputUrl.value);
const updateCalled =
testProxy.handler.whenCalled('updateMostVisitedTile');
inputUrl.value = 'updated-url';
saveButton.click();
const [url, newUrl, newTitle] = await updateCalled;
assertEquals('https://updated-url/', newUrl.url);
});
test('toast shown when tile editted', async () => {
inputUrl.value = 'updated-url';
assertFalse(toastManager.isToastOpen);
saveButton.click();
await flushTasks();
assertTrue(toastManager.isToastOpen);
});
test('no toast when not editted', async () => {
assertFalse(toastManager.isToastOpen);
saveButton.click();
await flushTasks();
assertFalse(toastManager.isToastOpen);
});
test('edit a tile title', async () => {
assertEquals('b', inputName.value);
const updateCalled =
testProxy.handler.whenCalled('updateMostVisitedTile');
inputName.value = 'updated name';
saveButton.click();
const [url, newUrl, newTitle] = await updateCalled;
assertEquals('updated name', newTitle);
});
test('update not called when name and URL not changed', async () => {
// |updateMostVisitedTile| will be called only after either the title or
// url has changed.
const updateCalled =
testProxy.handler.whenCalled('updateMostVisitedTile');
saveButton.click();
// Reopen dialog and edit URL.
actionMenuButton.click();
mostVisited.$.actionMenuEdit.click();
inputUrl.value = 'updated-url';
saveButton.click();
const [url, newUrl, newTitle] = await updateCalled;
assertEquals('https://updated-url/', newUrl.url);
});
});
test('remove with action menu', async () => {
const {actionMenu, actionMenuRemove: removeButton} = mostVisited.$;
await addTiles(2);
const secondTile = queryTiles()[1];
const actionMenuButton = secondTile.querySelector('cr-icon-button');
assertFalse(actionMenu.open);
actionMenuButton.click();
assertTrue(actionMenu.open);
const deleteCalled = testProxy.handler.whenCalled('deleteMostVisitedTile');
assertFalse(toastManager.isToastOpen);
removeButton.click();
assertFalse(actionMenu.open);
assertEquals('https://b/', (await deleteCalled).url);
assertTrue(toastManager.isToastOpen);
});
});
// Copyright 2019 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 Test suite for the WebUI new tab page page. */
GEN_INCLUDE(['//chrome/test/data/webui/polymer_browser_test_base.js']);
GEN('#include "services/network/public/cpp/features.h"');
class NewTabPageBrowserTest extends PolymerTest {
/** @override */
get browsePreload() {
throw 'this is abstract and should be overriden by subclasses';
}
/** @override */
get extraLibraries() {
return [
'//third_party/mocha/mocha.js',
'//chrome/test/data/webui/mocha_adapter.js',
];
}
}
// eslint-disable-next-line no-var
var NewTabPageAppTest = class extends NewTabPageBrowserTest {
/** @override */
get browsePreload() {
return 'chrome://new-tab-page/test_loader.html?module=new_tab_page/app_test.js';
}
};
TEST_F('NewTabPageAppTest', 'All', function() {
mocha.run();
});
// eslint-disable-next-line no-var
var NewTabPageMostVisitedTest = class extends NewTabPageBrowserTest {
/** @override */
get browsePreload() {
return 'chrome://new-tab-page/test_loader.html?module=new_tab_page/most_visited_test.js';
}
};
TEST_F('NewTabPageMostVisitedTest', 'All', function() {
mocha.run();
});
// Copyright 2019 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 {TestBrowserProxy} from 'chrome://test/test_browser_proxy.m.js';
export class TestProxy {
constructor() {
/** @type {newTabPage.mojom.PageCallbackRouter} */
this.callbackRouter = new newTabPage.mojom.PageCallbackRouter();
/** @type {!newTabPage.mojom.PageRemote} */
this.callbackRouterRemote =
this.callbackRouter.$.bindNewPipeAndPassRemote();
/** @type {newTabPage.mojom.PageHandlerInterface} */
this.handler = new FakePageHandler(this.callbackRouterRemote);
}
}
/** @implements {newTabPage.mojom.PageHandlerInterface} */
class FakePageHandler {
/** @param {newTabPage.mojom.PageInterface} */
constructor(callbackRouterRemote) {
/** @private {newTabPage.mojom.PageInterface} */
this.callbackRouterRemote_ = callbackRouterRemote;
/** @private {TestBrowserProxy} */
this.callTracker_ = new TestBrowserProxy([
'addMostVisitedTile',
'deleteMostVisitedTile',
'reorderMostVisitedTile',
'restoreMostVisitedDefaults',
'undoMostVisitedTileAction',
'updateMostVisitedTile',
]);
}
/**
* @param {string} methodName
* @return {!Promise}
*/
whenCalled(methodName) {
return this.callTracker_.whenCalled(methodName);
}
/** @override */
addMostVisitedTile(url, title) {
this.callTracker_.methodCalled('addMostVisitedTile', [url, title]);
return true;
}
/** @override */
deleteMostVisitedTile(url) {
this.callTracker_.methodCalled('deleteMostVisitedTile', url);
return true;
}
/** @override */
reorderMostVisitedTile(url, newPos) {
this.callTracker_.methodCalled('reorderMostVisitedTile', [url, newPos]);
}
/** @override */
restoreMostVisitedDefaults() {
this.callTracker_.methodCalled('restoreMostVisitedDefaults');
}
/** @override */
undoMostVisitedTileAction() {
this.callTracker_.methodCalled('undoMostVisitedTileAction');
}
/** @override */
updateMostVisitedTile(url, newUrl, newTitle) {
this.callTracker_.methodCalled(
'updateMostVisitedTile', [url, newUrl, newTitle]);
return true;
}
}
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