// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {MostVisitedBrowserProxy} from 'chrome://resources/cr_components/most_visited/browser_proxy.js';
import {MostVisitedElement} from 'chrome://resources/cr_components/most_visited/most_visited.js';
import type {MostVisitedPageRemote, MostVisitedTile} from 'chrome://resources/cr_components/most_visited/most_visited.mojom-webui.js';
import {MostVisitedPageCallbackRouter, MostVisitedPageHandlerRemote} from 'chrome://resources/cr_components/most_visited/most_visited.mojom-webui.js';
import {MostVisitedWindowProxy} from 'chrome://resources/cr_components/most_visited/window_proxy.js';
import type {CrButtonElement} from 'chrome://resources/cr_elements/cr_button/cr_button.js';
import type {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import type {CrInputElement} from 'chrome://resources/cr_elements/cr_input/cr_input.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {isMac} from 'chrome://resources/js/platform.js';
import {TextDirection} from 'chrome://resources/mojo/mojo/public/mojom/base/text_direction.mojom-webui.js';
import {assertDeepEquals, assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {TestMock} from 'chrome://webui-test/test_mock.js';
import {microtasksFinished} from 'chrome://webui-test/test_util.js';
import {$$, assertStyle, keydown} from './most_visited_test_support.js';
let mostVisited: MostVisitedElement;
let windowProxy: TestMock<MostVisitedWindowProxy>&MostVisitedWindowProxy;
let handler: TestMock<MostVisitedPageHandlerRemote>&
MostVisitedPageHandlerRemote;
let callbackRouterRemote: MostVisitedPageRemote;
const mediaListenerLists: Map<number, FakeMediaQueryList> = new Map();
function queryAll<E extends Element = Element>(q: string): E[] {
return Array.from(mostVisited.shadowRoot!.querySelectorAll<E>(q));
}
function queryTiles(): HTMLAnchorElement[] {
return queryAll<HTMLAnchorElement>('.tile');
}
function queryHiddenTiles(): HTMLAnchorElement[] {
return queryAll<HTMLAnchorElement>('.tile[hidden]');
}
function assertTileLength(length: number) {
assertEquals(length, queryTiles().length);
}
function assertHiddenTileLength(length: number) {
assertEquals(length, queryHiddenTiles().length);
}
async function addTiles(
n: number|MostVisitedTile[], customLinksEnabled: boolean = true,
visible: boolean = true) {
const tiles = Array.isArray(n) ? n : Array(n).fill(0).map((_x, i) => {
const char = String.fromCharCode(i + /* 'a' */ 97);
return {
title: char,
titleDirection: TextDirection.LEFT_TO_RIGHT,
url: {url: `https://${char}/`},
source: i,
titleSource: i,
isQueryTile: false,
};
});
callbackRouterRemote.setMostVisitedInfo({
customLinksEnabled,
tiles,
visible,
});
await callbackRouterRemote.$.flushForTesting();
await microtasksFinished();
}
function assertAddShortcutHidden() {
assertTrue(mostVisited.$.addShortcut.hidden);
}
function assertAddShortcutShown() {
assertFalse(mostVisited.$.addShortcut.hidden);
}
function createBrowserProxy() {
handler = TestMock.fromClass(MostVisitedPageHandlerRemote);
const callbackRouter = new MostVisitedPageCallbackRouter();
MostVisitedBrowserProxy.setInstance(
new MostVisitedBrowserProxy(handler, callbackRouter));
callbackRouterRemote = callbackRouter.$.bindNewPipeAndPassRemote();
handler.setResultFor('addMostVisitedTile', Promise.resolve({
success: true,
}));
handler.setResultFor('updateMostVisitedTile', Promise.resolve({
success: true,
}));
}
class FakeMediaQueryList extends EventTarget implements MediaQueryList {
matches: boolean = false;
media: string;
constructor(query: string) {
super();
this.media = query;
}
addListener() {}
removeListener() {}
onchange() {}
}
function createWindowProxy() {
windowProxy = TestMock.fromClass(MostVisitedWindowProxy);
windowProxy.setResultMapperFor('matchMedia', (query: string) => {
const result = query.match(/\(min-width: (\d+)px\)/);
assertTrue(!!result);
const mediaListenerList = new FakeMediaQueryList(query);
mediaListenerLists.set(parseInt(result![1]!), mediaListenerList);
return mediaListenerList;
});
MostVisitedWindowProxy.setInstance(windowProxy);
}
function updateScreenWidth(isWide: boolean, isMedium: boolean) {
mediaListenerLists.forEach(list => list.matches = false);
const mediaListenerWideWidth =
mediaListenerLists.get(Math.max(...mediaListenerLists.keys()));
const mediaListenerMediumWidth = mediaListenerLists.get(560);
assertTrue(!!mediaListenerWideWidth);
assertTrue(!!mediaListenerMediumWidth);
mediaListenerWideWidth!.matches = isWide;
mediaListenerMediumWidth!.matches = isMedium;
mediaListenerMediumWidth!.dispatchEvent(new Event('change'));
return microtasksFinished();
}
function wide() {
return updateScreenWidth(true, true);
}
function medium() {
return updateScreenWidth(false, true);
}
function narrow() {
return updateScreenWidth(false, false);
}
function leaveUrlInput() {
$$(mostVisited, '#dialogInputUrl').dispatchEvent(new Event('blur'));
return microtasksFinished();
}
function setUpTest(singleRow: boolean, reflowOnOverflow: boolean) {
document.body.innerHTML = window.trustedTypes!.emptyHTML;
createBrowserProxy();
createWindowProxy();
mostVisited = new MostVisitedElement();
mostVisited.singleRow = singleRow;
mostVisited.reflowOnOverflow = reflowOnOverflow;
document.body.appendChild(mostVisited);
assertEquals(1, handler.getCallCount('updateMostVisitedInfo'));
return wide();
}
suite('General', () => {
setup(async () => {
await setUpTest(/*singleRow=*/ false, /*reflowOnOverflow=*/ false);
});
test('empty shows add shortcut only', async () => {
assertAddShortcutHidden();
await addTiles(0);
assertEquals(0, queryTiles().length);
assertAddShortcutShown();
});
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);
keydown(mostVisited.$.addShortcut, '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: ' '}));
mostVisited.$.addShortcut.dispatchEvent(
new KeyboardEvent('keyup', {key: ' '}));
assertTrue(mostVisited.$.dialog.open);
});
});
function createLayoutsSuite(singleRow: boolean, reflowOnOverflow: boolean) {
setup(async () => {
await setUpTest(singleRow, reflowOnOverflow);
});
test('four tiles fit on one line with addShortcut', async () => {
await addTiles(4);
assertEquals(4, queryTiles().length);
assertAddShortcutShown();
const tops = queryAll<HTMLElement>('.tile, #addShortcut')
.map(({offsetTop}) => offsetTop);
assertEquals(5, tops.length);
tops.forEach(top => {
assertEquals(tops[0], top);
});
});
test('five tiles are displayed with addShortcut', async () => {
await addTiles(5);
assertEquals(5, queryTiles().length);
assertAddShortcutShown();
const tops = queryAll<HTMLElement>('.tile, #addShortcut')
.map(({offsetTop}) => offsetTop);
assertEquals(6, tops.length);
const firstRowTop = tops[0];
const secondRowTop = tops[3];
if (singleRow) {
assertEquals(firstRowTop, secondRowTop);
} else {
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 with addShortcut', async () => {
await addTiles(9);
assertEquals(9, queryTiles().length);
assertAddShortcutShown();
const tops = queryAll<HTMLElement>('.tile, #addShortcut')
.map(({offsetTop}) => offsetTop);
assertEquals(10, tops.length);
const firstRowTop = tops[0];
const secondRowTop = tops[5];
if (singleRow) {
assertEquals(firstRowTop, secondRowTop);
} else {
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 without addShortcut', async () => {
await addTiles(10);
assertEquals(10, queryTiles().length);
assertAddShortcutHidden();
const tops =
queryAll<HTMLElement>('.tile:not([hidden])').map(a => a.offsetTop);
assertEquals(10, tops.length);
const firstRowTop = tops[0];
const secondRowTop = tops[5];
if (singleRow) {
assertEquals(firstRowTop, secondRowTop);
} else {
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);
assertAddShortcutHidden();
});
test('eight tiles is the max (customLinksEnabled=false)', async () => {
await addTiles(11, /* customLinksEnabled */ true);
assertEquals(10, queryTiles().length);
assertEquals(0, queryAll('.tile[hidden]').length);
assertAddShortcutHidden();
await addTiles(11, /* customLinksEnabled */ false);
assertEquals(8, queryTiles().length);
assertEquals(0, queryAll('.tile[hidden]').length);
assertAddShortcutHidden();
await addTiles(11, /* customLinksEnabled */ true);
assertEquals(10, queryTiles().length);
assertEquals(0, queryAll('.tile[hidden]').length);
});
test('7 tiles and no add shortcut (customLinksEnabled=false)', async () => {
await addTiles(7, /* customLinksEnabled */ true);
assertAddShortcutShown();
await addTiles(7, /* customLinksEnabled */ false);
assertAddShortcutHidden();
await addTiles(7, /* customLinksEnabled */ true);
assertAddShortcutShown();
});
test('no tiles shown when (visible=false)', async () => {
await addTiles(1);
assertEquals(1, queryTiles().length);
assertEquals(0, queryAll('.tile[hidden]').length);
assertTrue(mostVisited.hasAttribute('visible_'));
assertFalse(mostVisited.$.container.hidden);
await addTiles(1, /* customLinksEnabled */ true, /* visible */ false);
assertEquals(1, queryTiles().length);
assertEquals(0, queryAll('.tile[hidden]').length);
assertFalse(mostVisited.hasAttribute('visible_'));
assertTrue(mostVisited.$.container.hidden);
await addTiles(1, /* customLinksEnabled */ true, /* visible */ true);
assertEquals(1, queryTiles().length);
assertEquals(0, queryAll('.tile[hidden]').length);
assertTrue(mostVisited.hasAttribute('visible_'));
assertFalse(mostVisited.$.container.hidden);
});
}
function createLayoutsWidthsSuite(singleRow: boolean) {
suite('test various widths', () => {
setup(async () => {
await setUpTest(singleRow, false);
});
test('six / three is max for narrow', async () => {
await addTiles(7);
await medium();
assertTileLength(7);
assertHiddenTileLength(singleRow ? 3 : 0);
await narrow();
assertTileLength(7);
assertHiddenTileLength(singleRow ? 4 : 1);
await medium();
assertTileLength(7);
assertHiddenTileLength(singleRow ? 3 : 0);
});
test('eight / four is max for medium', async () => {
await addTiles(8);
await narrow();
assertTileLength(8);
assertHiddenTileLength(singleRow ? 5 : 2);
await medium();
assertTileLength(8);
assertHiddenTileLength(singleRow ? 4 : 0);
await narrow();
assertTileLength(8);
assertHiddenTileLength(singleRow ? 5 : 2);
});
test('eight is max for wide', async () => {
await addTiles(8);
await narrow();
assertTileLength(8);
assertHiddenTileLength(singleRow ? 5 : 2);
await wide();
assertTileLength(8);
assertHiddenTileLength(0);
await narrow();
assertTileLength(8);
assertHiddenTileLength(singleRow ? 5 : 2);
});
test('hide add shortcut (narrow)', async () => {
await addTiles(6);
await medium();
if (singleRow) {
assertAddShortcutHidden();
} else {
assertAddShortcutShown();
}
await narrow();
assertAddShortcutHidden();
await medium();
if (singleRow) {
assertAddShortcutHidden();
} else {
assertAddShortcutShown();
}
});
test('hide add shortcut with 8 tiles (medium)', async () => {
await addTiles(8);
await wide();
assertAddShortcutShown();
await medium();
assertAddShortcutHidden();
await wide();
assertAddShortcutShown();
});
test('hide add shortcut with 9 tiles (medium)', async () => {
await addTiles(9);
await wide();
assertAddShortcutShown();
await addTiles(10);
assertAddShortcutHidden();
});
if (singleRow) {
test('shows correct number of tiles for all widths', async () => {
await addTiles(12);
mediaListenerLists.forEach(list => list.matches = false);
[...mediaListenerLists.keys()]
.sort((a, b) => a - b)
.forEach(async (width, i) => {
const list = mediaListenerLists.get(width)!;
list.matches = true;
list.dispatchEvent(new Event('change'));
await microtasksFinished();
assertHiddenTileLength(6 - i);
});
});
}
});
}
function rowCount(): number {
return Number(getComputedStyle(mostVisited.$.container)
.getPropertyValue('--row-count'));
}
function columnCount(): number {
return Number(getComputedStyle(mostVisited.$.container)
.getPropertyValue('--column-count'));
}
function createLayoutsWidthsReflowSuite(singleRow: boolean) {
suite('test reflow on various widths', () => {
setup(async () => {
await setUpTest(singleRow, /*reflowOnOverflow=*/ true);
});
test('No hidden tiles', async () => {
await addTiles(7);
await updateScreenWidth(false, true);
assertTileLength(7);
assertHiddenTileLength(0);
await updateScreenWidth(false, false);
assertTileLength(7);
assertHiddenTileLength(0);
assertAddShortcutShown();
});
test(
'Eight tiles + shortcut reflow to 3c x 3r in narrow layout',
async () => {
await narrow();
await addTiles(8);
assertAddShortcutShown();
assertEquals(columnCount(), 3);
assertEquals(rowCount(), 3);
});
test(
'Eight tiles + shortcut reflow to 4c x 3r in medium layout',
async () => {
await medium();
await addTiles(8);
assertAddShortcutShown();
assertEquals(columnCount(), 4);
assertEquals(rowCount(), 3);
});
test('Eight tiles + shortcut reflow in wide layout', async () => {
await wide();
await addTiles(8);
assertAddShortcutShown();
assertEquals(columnCount(), singleRow ? 9 : 5);
assertEquals(rowCount(), singleRow ? 1 : 2);
});
});
}
suite('Layouts', () => {
suite('double row', () => {
createLayoutsSuite(/*singleRow=*/ false, /*reflowOnOverflow=*/ false);
createLayoutsWidthsSuite(/*singleRow=*/ false);
});
suite('single row', () => {
createLayoutsSuite(/*singleRow=*/ true, /*reflowOnOverflow=*/ false);
createLayoutsWidthsSuite(/*singleRow=*/ true);
});
});
suite('Reflow Layouts', () => {
suite('double row', () => {
createLayoutsSuite(/*singleRow=*/ false, /*reflowOnOverflow=*/ true);
createLayoutsWidthsReflowSuite(/*singleRow=*/ false);
});
suite('single row', () => {
createLayoutsSuite(/*singleRow=*/ false, /*reflowOnOverflow=*/ true);
createLayoutsWidthsReflowSuite(/*singleRow=*/ true);
});
});
suite('LoggingAndUpdates', () => {
setup(async () => {
await setUpTest(/*singleRow=*/ false, /*reflowOnOverflow=*/ false);
});
test('rendering tiles logs event', async () => {
// Clear promise resolvers created during setup.
handler.reset();
// Arrange.
windowProxy.setResultFor('now', 123);
// Act.
await addTiles(2);
// Assert.
const [tiles, time] =
await handler.whenCalled('onMostVisitedTilesRendered');
assertEquals(time, 123);
assertEquals(tiles.length, 2);
assertDeepEquals(tiles[0], {
title: 'a',
titleDirection: TextDirection.LEFT_TO_RIGHT,
url: {url: 'https://a/'},
source: 0,
titleSource: 0,
isQueryTile: false,
});
assertDeepEquals(tiles[1], {
title: 'b',
titleDirection: TextDirection.LEFT_TO_RIGHT,
url: {url: 'https://b/'},
source: 1,
titleSource: 1,
isQueryTile: false,
});
});
test('clicking tile logs event', async () => {
// Arrange.
await addTiles(1);
// Act.
const tileLink = queryTiles()[0]!.querySelector('a')!;
// Prevent triggering a navigation, which would break the test.
tileLink.href = '#';
tileLink.click();
// Assert.
const [tile, index] =
await handler.whenCalled('onMostVisitedTileNavigation');
assertEquals(index, 0);
assertDeepEquals(tile, {
title: 'a',
titleDirection: TextDirection.LEFT_TO_RIGHT,
url: {url: 'https://a/'},
source: 0,
titleSource: 0,
isQueryTile: false,
});
});
test('making tab visible refreshes most visited tiles', () => {
// Arrange.
handler.resetResolver('updateMostVisitedInfo');
// Act.
document.dispatchEvent(new Event('visibilitychange'));
// Assert.
assertEquals(1, handler.getCallCount('updateMostVisitedInfo'));
});
});
suite('Modification', () => {
suiteSetup(() => {
loadTimeData.overrideValues({
invalidUrl: 'Type a valid URL',
linkAddedMsg: 'Shortcut added',
linkCantCreate: 'Can\'t create shortcut',
linkEditedMsg: 'Shortcut edited',
restoreDefaultLinks: 'Restore default shortcuts',
shortcutAlreadyExists: 'Shortcut already exists',
});
});
setup(async () => {
await setUpTest(/*singleRow=*/ false, /*reflowOnOverflow=*/ false);
});
suite('add dialog', () => {
let dialog: CrDialogElement;
let inputName: CrInputElement;
let inputUrl: CrInputElement;
let saveButton: CrButtonElement;
let cancelButton: CrButtonElement;
setup(async () => {
dialog = mostVisited.$.dialog;
inputName = $$<CrInputElement>(mostVisited, '#dialogInputName')!;
inputUrl = $$<CrInputElement>(mostVisited, '#dialogInputUrl')!;
saveButton = dialog.querySelector('.action-button')!;
cancelButton = dialog.querySelector('.cancel-button')!;
await microtasksFinished();
mostVisited.$.addShortcut.click();
assertTrue(dialog.open);
});
test('inputs are initially empty', () => {
assertEquals('', inputName.value);
assertEquals('', inputUrl.value);
});
test('saveButton is enabled with URL is not empty', async () => {
assertTrue(saveButton.disabled);
inputName.value = 'name';
await inputName.updateComplete;
assertTrue(saveButton.disabled);
inputUrl.value = 'url';
await inputUrl.updateComplete;
assertFalse(saveButton.disabled);
inputUrl.value = '';
await inputUrl.updateComplete;
assertTrue(saveButton.disabled);
inputUrl.value = 'url';
await inputUrl.updateComplete;
assertFalse(saveButton.disabled);
inputUrl.value = ' \n\n\n ';
await inputUrl.updateComplete;
assertTrue(saveButton.disabled);
});
test('cancel closes dialog', () => {
assertTrue(dialog.open);
cancelButton.click();
assertFalse(dialog.open);
});
test('inputs are clear after dialog reuse', async () => {
inputName.value = 'name';
inputUrl.value = 'url';
await Promise.all([inputName.updateComplete, inputUrl.updateComplete]);
cancelButton.click();
mostVisited.$.addShortcut.click();
await microtasksFinished();
assertEquals('', inputName.value);
assertEquals('', inputUrl.value);
});
test('use URL input for title when title empty', async () => {
inputUrl.value = 'url';
const addCalled = handler.whenCalled('addMostVisitedTile');
await inputUrl.updateComplete;
saveButton.click();
const [_url, title] = await addCalled;
assertEquals('url', title);
});
test('toast shown on save', async () => {
inputUrl.value = 'url';
await inputUrl.updateComplete;
assertFalse(mostVisited.$.toast.open);
const addCalled = handler.whenCalled('addMostVisitedTile');
saveButton.click();
await addCalled;
assertTrue(mostVisited.$.toast.open);
});
test('toast has undo buttons when action successful', async () => {
handler.setResultFor('addMostVisitedTile', Promise.resolve({
success: true,
}));
inputUrl.value = 'url';
await inputUrl.updateComplete;
saveButton.click();
await handler.whenCalled('addMostVisitedTile');
await microtasksFinished();
assertFalse($$<HTMLElement>(mostVisited, '#undo')!.hidden);
});
test('toast has no undo buttons when action successful', async () => {
handler.setResultFor('addMostVisitedTile', Promise.resolve({
success: false,
}));
inputUrl.value = 'url';
await inputUrl.updateComplete;
saveButton.click();
await handler.whenCalled('addMostVisitedTile');
await microtasksFinished();
assertFalse(!!$$(mostVisited, '#undo'));
});
test('save name and URL', async () => {
inputName.value = 'name';
inputUrl.value = 'https://url/';
await Promise.all([inputName.updateComplete, inputUrl.updateComplete]);
const addCalled = handler.whenCalled('addMostVisitedTile');
saveButton.click();
const [{url}, title] = await addCalled;
assertEquals('name', title);
assertEquals('https://url/', url);
});
test('dialog closes on save', async () => {
inputUrl.value = 'url';
await inputUrl.updateComplete;
assertTrue(dialog.open);
saveButton.click();
assertFalse(dialog.open);
});
test('https:// is added if no scheme is used', async () => {
inputUrl.value = 'url';
await inputUrl.updateComplete;
const addCalled = handler.whenCalled('addMostVisitedTile');
saveButton.click();
const [{url}, _title] = await addCalled;
assertEquals('https://url/', url);
});
test('http is a valid scheme', async () => {
assertTrue(saveButton.disabled);
inputUrl.value = 'http://url';
await inputUrl.updateComplete;
const addCalled = handler.whenCalled('addMostVisitedTile');
saveButton.click();
await addCalled;
assertFalse(saveButton.disabled);
});
test('https is a valid scheme', async () => {
inputUrl.value = 'https://url';
await inputUrl.updateComplete;
const addCalled = handler.whenCalled('addMostVisitedTile');
saveButton.click();
await addCalled;
});
test('chrome is not a valid scheme', async () => {
assertTrue(saveButton.disabled);
inputUrl.value = 'chrome://url';
await inputUrl.updateComplete;
assertFalse(inputUrl.invalid);
await leaveUrlInput();
assertTrue(inputUrl.invalid);
assertTrue(saveButton.disabled);
});
test('invalid cleared when text entered', async () => {
inputUrl.value = '%';
await inputUrl.updateComplete;
assertFalse(inputUrl.invalid);
await leaveUrlInput();
assertTrue(inputUrl.invalid);
assertEquals('Type a valid URL', inputUrl.errorMessage);
inputUrl.value = '';
await inputUrl.updateComplete;
assertFalse(inputUrl.invalid);
});
test('shortcut already exists', async () => {
await addTiles(2);
inputUrl.value = 'b';
await inputUrl.updateComplete;
assertFalse(inputUrl.invalid);
await leaveUrlInput();
assertTrue(inputUrl.invalid);
assertEquals('Shortcut already exists', inputUrl.errorMessage);
inputUrl.value = 'c';
await inputUrl.updateComplete;
assertFalse(inputUrl.invalid);
await leaveUrlInput();
assertFalse(inputUrl.invalid);
inputUrl.value = '%';
await inputUrl.updateComplete;
assertFalse(inputUrl.invalid);
await leaveUrlInput();
assertTrue(inputUrl.invalid);
assertEquals('Type a valid URL', inputUrl.errorMessage);
});
});
test('open edit dialog', async () => {
await addTiles(2);
const actionMenu = mostVisited.$.actionMenu;
const dialog = mostVisited.$.dialog;
assertFalse(actionMenu.open);
queryTiles()[0]!.querySelector<HTMLElement>('#actionMenuButton')!.click();
assertTrue(actionMenu.open);
assertFalse(dialog.open);
$$<HTMLElement>(mostVisited, '#actionMenuEdit')!.click();
assertFalse(actionMenu.open);
assertTrue(dialog.open);
});
suite('edit dialog', () => {
let actionMenuButton: HTMLElement;
let inputName: CrInputElement;
let inputUrl: CrInputElement;
let saveButton: HTMLElement;
let tile: HTMLAnchorElement;
setup(async () => {
inputName = $$<CrInputElement>(mostVisited, '#dialogInputName')!;
inputUrl = $$<CrInputElement>(mostVisited, '#dialogInputUrl')!;
const dialog = mostVisited.$.dialog;
saveButton = dialog.querySelector('.action-button')!;
await addTiles(2);
tile = queryTiles()[1]!;
actionMenuButton = tile.querySelector<HTMLElement>('#actionMenuButton')!;
actionMenuButton.click();
$$<HTMLElement>(mostVisited, '#actionMenuEdit')!.click();
});
test('edit a tile URL', async () => {
assertEquals('https://b/', inputUrl.value);
const updateCalled = handler.whenCalled('updateMostVisitedTile');
inputUrl.value = 'updated-url';
await inputUrl.updateComplete;
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';
await inputUrl.updateComplete;
assertFalse(mostVisited.$.toast.open);
saveButton.click();
await handler.whenCalled('updateMostVisitedTile');
assertTrue(mostVisited.$.toast.open);
});
test('no toast when not editted', async () => {
assertFalse(mostVisited.$.toast.open);
saveButton.click();
assertFalse(mostVisited.$.toast.open);
});
test('edit a tile title', async () => {
assertEquals('b', inputName.value);
const updateCalled = handler.whenCalled('updateMostVisitedTile');
inputName.value = 'updated name';
await inputName.updateComplete;
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 = handler.whenCalled('updateMostVisitedTile');
saveButton.click();
// Reopen dialog and edit URL.
actionMenuButton.click();
$$<HTMLElement>(mostVisited, '#actionMenuEdit')!.click();
inputUrl.value = 'updated-url';
await inputUrl.updateComplete;
saveButton.click();
const [_url, newUrl, _newTitle] = await updateCalled;
assertEquals('https://updated-url/', newUrl.url);
});
test('shortcut already exists', async () => {
inputUrl.value = 'a';
await inputUrl.updateComplete;
assertFalse(inputUrl.invalid);
await leaveUrlInput();
assertTrue(inputUrl.invalid);
assertEquals('Shortcut already exists', inputUrl.errorMessage);
// The shortcut being editted has a URL of https://b/. Entering the same
// URL is not an error.
inputUrl.value = 'b';
await inputUrl.updateComplete;
assertFalse(inputUrl.invalid);
await leaveUrlInput();
assertFalse(inputUrl.invalid);
});
});
test('remove with action menu', async () => {
const actionMenu = mostVisited.$.actionMenu;
const removeButton = $$<HTMLElement>(mostVisited, '#actionMenuRemove')!;
await addTiles(2);
const secondTile = queryTiles()[1]!;
const actionMenuButton =
secondTile.querySelector<HTMLElement>('#actionMenuButton')!;
assertFalse(actionMenu.open);
actionMenuButton.click();
assertTrue(actionMenu.open);
const deleteCalled = handler.whenCalled('deleteMostVisitedTile');
assertFalse(mostVisited.$.toast.open);
removeButton.click();
assertFalse(actionMenu.open);
assertEquals('https://b/', (await deleteCalled).url);
assertTrue(mostVisited.$.toast.open);
// Toast buttons are visible.
assertTrue(!!$$(mostVisited, '#undo'));
assertTrue(!!$$(mostVisited, '#restore'));
});
test('remove query with action menu', async () => {
const actionMenu = mostVisited.$.actionMenu;
const removeButton = $$<HTMLElement>(mostVisited, '#actionMenuRemove')!;
await addTiles([{
title: 'title',
titleDirection: TextDirection.LEFT_TO_RIGHT,
url: {url: 'https://search-url/'},
source: 0,
titleSource: 0,
isQueryTile: true,
}]);
const actionMenuButton =
queryTiles()[0]!.querySelector<HTMLElement>('#actionMenuButton')!;
assertFalse(actionMenu.open);
actionMenuButton.click();
assertTrue(actionMenu.open);
const deleteCalled = handler.whenCalled('deleteMostVisitedTile');
assertFalse(mostVisited.$.toast.open);
removeButton.click();
assertEquals('https://search-url/', (await deleteCalled).url);
assertTrue(mostVisited.$.toast.open);
// Toast buttons are visible.
assertTrue(!!$$(mostVisited, '#undo'));
assertTrue(!!$$(mostVisited, '#restore'));
});
test('remove with icon button (customLinksEnabled=false)', async () => {
await addTiles(1, /* customLinksEnabled */ false);
const removeButton =
queryTiles()[0]!.querySelector<HTMLElement>('#removeButton')!;
const deleteCalled = handler.whenCalled('deleteMostVisitedTile');
assertFalse(mostVisited.$.toast.open);
removeButton.click();
assertEquals('https://a/', (await deleteCalled).url);
assertTrue(mostVisited.$.toast.open);
// Toast buttons are visible.
assertTrue(!!$$(mostVisited, '#undo'));
assertTrue(!!$$(mostVisited, '#restore'));
});
test('remove query with icon button (customLinksEnabled=false)', async () => {
await addTiles(
[{
title: 'title',
titleDirection: TextDirection.LEFT_TO_RIGHT,
url: {url: 'https://search-url/'},
source: 0,
titleSource: 0,
isQueryTile: true,
}],
/* customLinksEnabled */ false);
const removeButton =
queryTiles()[0]!.querySelector<HTMLElement>('#removeButton')!;
const deleteCalled = handler.whenCalled('deleteMostVisitedTile');
assertFalse(mostVisited.$.toast.open);
removeButton.click();
assertEquals('https://search-url/', (await deleteCalled).url);
assertTrue(mostVisited.$.toast.open);
// Toast buttons are not visible.
assertFalse(!!$$(mostVisited, '#undo'));
assertFalse(!!$$(mostVisited, '#restore'));
});
test('tile url is set to href of <a>', async () => {
await addTiles(1);
const tile = queryTiles()[0]!;
assertEquals('https://a/', tile!.querySelector('a')!.href);
});
test('delete first tile', async () => {
await addTiles(1);
const tile = queryTiles()[0]!;
const deleteCalled = handler.whenCalled('deleteMostVisitedTile');
assertFalse(mostVisited.$.toast.open);
keydown(tile, 'Delete');
assertEquals('https://a/', (await deleteCalled).url);
assertTrue(mostVisited.$.toast.open);
});
test('ctrl+z triggers undo and hides toast', async () => {
const toast = mostVisited.$.toast;
assertFalse(toast.open);
// Add a tile and remove it to show the toast.
await addTiles(1);
const tile = queryTiles()[0]!;
keydown(tile, 'Delete');
await handler.whenCalled('deleteMostVisitedTile');
assertTrue(toast.open);
const undoCalled = handler.whenCalled('undoMostVisitedTileAction');
mostVisited.dispatchEvent(new KeyboardEvent('keydown', {
bubbles: true,
ctrlKey: !isMac,
key: 'z',
metaKey: isMac,
}));
await undoCalled;
assertFalse(toast.open);
});
test('ctrl+z does nothing if toast buttons are not showing', async () => {
const toast = mostVisited.$.toast;
assertFalse(toast.open);
// A failed attempt at adding a shortcut to show the toast with no buttons.
handler.setResultFor('addMostVisitedTile', Promise.resolve({
success: false,
}));
mostVisited.$.addShortcut.click();
await microtasksFinished();
const inputUrl = $$<CrInputElement>(mostVisited, '#dialogInputUrl')!;
inputUrl.value = 'url';
await inputUrl.updateComplete;
const saveButton =
mostVisited.$.dialog.querySelector<HTMLElement>('.action-button')!;
saveButton.click();
await handler.whenCalled('addMostVisitedTile');
assertTrue(toast.open);
mostVisited.dispatchEvent(new KeyboardEvent('keydown', {
bubbles: true,
ctrlKey: !isMac,
key: 'z',
metaKey: isMac,
}));
await microtasksFinished();
assertEquals(0, handler.getCallCount('undoMostVisitedTileAction'));
assertTrue(toast.open);
});
test('toast restore defaults button', async () => {
const wait = handler.whenCalled('restoreMostVisitedDefaults');
const toast = mostVisited.$.toast;
assertFalse(toast.open);
// Add a tile and remove it to show the toast.
await addTiles(1);
const tile = queryTiles()[0]!;
keydown(tile, 'Delete');
await handler.whenCalled('deleteMostVisitedTile');
assertTrue(toast.open);
toast.querySelector<HTMLElement>('#restore')!.click();
await wait;
assertFalse(toast.open);
});
test('toast undo button', async () => {
const wait = handler.whenCalled('undoMostVisitedTileAction');
const toast = mostVisited.$.toast;
assertFalse(toast.open);
// Add a tile and remove it to show the toast.
await addTiles(1);
const tile = queryTiles()[0]!;
keydown(tile, 'Delete');
await handler.whenCalled('deleteMostVisitedTile');
assertTrue(toast.open);
toast.querySelector<HTMLElement>('#undo')!.click();
await wait;
assertFalse(toast.open);
});
});
function createDragAndDropSuite(singleRow: boolean, reflowOnOverflow: boolean) {
setup(async () => {
await setUpTest(singleRow, reflowOnOverflow);
});
test('drag first tile to second position', async () => {
await addTiles(2);
const tiles = queryTiles();
const first = tiles[0]!;
const second = tiles[1]!;
assertEquals('https://a/', first.querySelector('a')!.href);
assertTrue(first.draggable);
assertEquals('https://b/', second.querySelector('a')!.href);
assertTrue(second.draggable);
const firstRect = first.getBoundingClientRect();
const secondRect = second.getBoundingClientRect();
first.dispatchEvent(new DragEvent('dragstart', {
clientX: firstRect.x + firstRect.width / 2,
clientY: firstRect.y + firstRect.height / 2,
}));
const reorderCalled = handler.whenCalled('reorderMostVisitedTile');
document.dispatchEvent(new DragEvent('drop', {
clientX: secondRect.x + 1,
clientY: secondRect.y + 1,
}));
document.dispatchEvent(new DragEvent('dragend', {
clientX: secondRect.x + 1,
clientY: secondRect.y + 1,
}));
await mostVisited.updateComplete;
const [url, newPos] = await reorderCalled;
assertEquals('https://a/', url.url);
assertEquals(1, newPos);
const [newFirst, newSecond] = queryTiles();
assertEquals('https://b/', newFirst!.querySelector('a')!.href);
assertEquals('https://a/', newSecond!.querySelector('a')!.href);
});
test('drag second tile to first position', async () => {
await addTiles(2);
const tiles = queryTiles();
const first = tiles[0]!;
const second = tiles[1]!;
assertEquals('https://a/', first.querySelector('a')!.href);
assertTrue(first.draggable);
assertEquals('https://b/', second.querySelector('a')!.href);
assertTrue(second.draggable);
const firstRect = first.getBoundingClientRect();
const secondRect = second.getBoundingClientRect();
second.dispatchEvent(new DragEvent('dragstart', {
clientX: secondRect.x + secondRect.width / 2,
clientY: secondRect.y + secondRect.height / 2,
}));
const reorderCalled = handler.whenCalled('reorderMostVisitedTile');
document.dispatchEvent(new DragEvent('drop', {
clientX: firstRect.x + 1,
clientY: firstRect.y + 1,
}));
document.dispatchEvent(new DragEvent('dragend', {
clientX: firstRect.x + 1,
clientY: firstRect.y + 1,
}));
await mostVisited.updateComplete;
const [url, newPos] = await reorderCalled;
assertEquals('https://b/', url.url);
assertEquals(0, newPos);
const [newFirst, newSecond] = queryTiles();
assertEquals('https://b/', newFirst!.querySelector('a')!.href);
assertEquals('https://a/', newSecond!.querySelector('a')!.href);
});
test('most visited tiles cannot be reordered', async () => {
await addTiles(2, /* customLinksEnabled= */ false);
const tiles = queryTiles();
const first = tiles[0]!;
const second = tiles[1]!;
assertEquals('https://a/', first.querySelector('a')!.href);
assertTrue(first.draggable);
assertEquals('https://b/', second.querySelector('a')!.href);
assertTrue(second.draggable);
const firstRect = first.getBoundingClientRect();
const secondRect = second.getBoundingClientRect();
first.dispatchEvent(new DragEvent('dragstart', {
clientX: firstRect.x + firstRect.width / 2,
clientY: firstRect.y + firstRect.height / 2,
}));
document.dispatchEvent(new DragEvent('drop', {
clientX: secondRect.x + 1,
clientY: secondRect.y + 1,
}));
document.dispatchEvent(new DragEvent('dragend', {
clientX: secondRect.x + 1,
clientY: secondRect.y + 1,
}));
await mostVisited.updateComplete;
assertEquals(0, handler.getCallCount('reorderMostVisitedTile'));
const [newFirst, newSecond] = queryTiles();
assertEquals('https://a/', newFirst!.querySelector('a')!.href);
assertEquals('https://b/', newSecond!.querySelector('a')!.href);
});
}
suite('DragAndDrop', () => {
suite('double row', () => {
createDragAndDropSuite(/*singleRow=*/ false, /*reflowOnOverflow=*/ false);
createDragAndDropSuite(/*singleRow=*/ false, /*reflowOnOverflow=*/ true);
});
suite('single row', () => {
createDragAndDropSuite(/*singleRow=*/ true, /*reflowOnOverflow=*/ false);
createDragAndDropSuite(/*singleRow=*/ true, /*reflowOnOverflow=*/ true);
});
});
suite('Theming', () => {
setup(async () => {
await setUpTest(/*singleRow=*/ false, /*reflowOnOverflow=*/ false);
});
test('RIGHT_TO_LEFT tile title text direction', async () => {
await addTiles([{
title: 'title',
titleDirection: TextDirection.RIGHT_TO_LEFT,
url: {url: 'https://url/'},
source: 0,
titleSource: 0,
isQueryTile: false,
}]);
const tile = queryTiles()[0]!;
const titleElement = tile.querySelector('.tile-title')!;
assertEquals('rtl', window.getComputedStyle(titleElement).direction);
});
test('LEFT_TO_RIGHT tile title text direction', async () => {
await addTiles([{
title: 'title',
titleDirection: TextDirection.LEFT_TO_RIGHT,
url: {url: 'https://url/'},
source: 0,
titleSource: 0,
isQueryTile: false,
}]);
const tile = queryTiles()[0]!;
const titleElement = tile.querySelector('.tile-title')!;
assertEquals('ltr', window.getComputedStyle(titleElement).direction);
});
test('setting color styles tile color', async () => {
// Act.
mostVisited.$.container.style.setProperty(
'--most-visited-text-color', 'blue');
mostVisited.$.container.style.setProperty('--tile-background-color', 'red');
await microtasksFinished();
// Assert.
queryAll('.tile-title').forEach(tile => {
assertStyle(tile, 'color', 'rgb(0, 0, 255)');
});
queryAll('.tile-icon').forEach(tile => {
assertStyle(tile, 'background-color', 'rgb(255, 0, 0)');
});
});
test('add shortcut white', async () => {
assertStyle(
$$(mostVisited, '#addShortcutIcon'), 'background-color',
'rgb(32, 33, 36)');
mostVisited.toggleAttribute('use-white-tile-icon_', true);
await microtasksFinished();
assertStyle(
$$(mostVisited, '#addShortcutIcon'), 'background-color',
'rgb(255, 255, 255)');
});
});
suite('Prerendering', () => {
suiteSetup(() => {});
setup(async () => {
await setUpTest(/*singleRow=*/ false, /*reflowOnOverflow=*/ false);
});
test('onMouseHover Trigger', async () => {
// Arrange.
await addTiles(1);
// Act.
const tileLink = queryTiles()[0]!.querySelector('a')!;
// Prevent triggering a navigation, which would break the test.
tileLink.href = '#';
// simulate a mousedown event.
const mouseEvent = document.createEvent('MouseEvents');
mouseEvent.initEvent('mouseenter', true, true);
tileLink.dispatchEvent(mouseEvent);
await microtasksFinished();
// Make sure both preconnect and prerender have been triggered.
await handler.whenCalled('preconnectMostVisitedTile');
await handler.whenCalled('prerenderMostVisitedTile');
});
test('onMouseDown Trigger', async () => {
// Arrange.
await addTiles(1);
// Act.
const tileLink = queryTiles()[0]!.querySelector('a')!;
// Prevent triggering a navigation, which would break the test.
tileLink.href = '#';
// simulate a mousedown event.
const mouseEvent = document.createEvent('MouseEvents');
mouseEvent.initEvent('mousedown', true, true);
tileLink.dispatchEvent(mouseEvent);
// Make sure Prerendering has been triggered.
await handler.whenCalled('prerenderMostVisitedTile');
});
test('prerender cancelation and retrigger', async () => {
// Arrange.
await addTiles(1);
// Act.
const tileLink = queryTiles()[0]!.querySelector('a')!;
// Prevent triggering a navigation, which would break the test.
tileLink.href = '#';
// simulate a mousedown event.
const mouseEnterEvent = document.createEvent('MouseEvents');
mouseEnterEvent.initEvent('mouseenter', true, true);
tileLink.dispatchEvent(mouseEnterEvent);
// Make sure Prerendering has been triggered
await handler.whenCalled('prerenderMostVisitedTile');
const mouseExitEvent = document.createEvent('MouseEvents');
mouseExitEvent.initEvent('mouseleave', true, true);
tileLink.dispatchEvent(mouseExitEvent);
// Make sure Prerendering has been canceled.
await handler.whenCalled('cancelPrerender');
tileLink.dispatchEvent(mouseEnterEvent);
// Make sure Prerendering can be re-triggered
await handler.whenCalled('prerenderMostVisitedTile');
tileLink.dispatchEvent(mouseExitEvent);
// Make sure Prerendering has been canceled.
await handler.whenCalled('cancelPrerender');
});
});