blob: c7a4faeb5f65a9fed730c7f6f13204b3dde989f6 [file] [log] [blame]
// 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 {$$, BrowserProxy, hexColorToSkColor, skColorToRgba} from 'chrome://new-tab-page/new_tab_page.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.m.js';
import {assertNotStyle, assertStyle, createTestProxy, keydown} from 'chrome://test/new_tab_page/test_support.js';
import {eventToPromise, flushTasks} from 'chrome://test/test_util.m.js';
/**
* @param {!Element} element
* @param {!Element} reference
* @return {!{top: number, right: number, bottom: number, left: number}}
*/
function getRelativePosition(element, reference) {
const referenceRect = reference.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
return {
top: elementRect.top - referenceRect.top,
right: elementRect.right - referenceRect.right,
bottom: elementRect.bottom - referenceRect.bottom,
left: elementRect.left - referenceRect.left,
};
}
/**
* @param {number} width
* @param {number} height
* @param {string} color
* @return {string}
*/
function createImageDataUrl(width, height, color) {
const svg = '<svg xmlns="http://www.w3.org/2000/svg" ' +
`width="${width}" height="${height}">` +
`<rect width="100%" height="100%" fill="${color}"/>` +
'</svg>';
return `data:image/svg+xml;base64,${btoa(svg)}`;
}
/**
* @param {number} width
* @param {number} height
* @return {!newTabPage.mojom.Doodle}
*/
function createImageDoodle(width, height) {
width = width || 500;
height = height || 200;
return {
image: {
light: {
imageUrl: {url: createImageDataUrl(width, height, 'red')},
shareButton: {
backgroundColor: {value: 0xddff0000},
x: 10,
y: 20,
iconUrl: {url: 'data:bar'},
},
width,
height,
backgroundColor: {value: 0xffffffff},
imageImpressionLogUrl: {url: 'https://log.com'},
},
dark: {
imageUrl: {url: createImageDataUrl(width, height, 'blue')},
shareButton: {
backgroundColor: {value: 0xee00ff00},
x: 30,
y: 40,
iconUrl: {url: 'data:foo'},
},
width,
height,
backgroundColor: {value: 0x000000ff},
imageImpressionLogUrl: {url: 'https://dark_log.com'},
},
onClickUrl: {url: 'https://foo.com'},
}
};
}
function createSuite(themeModeDoodlesEnabled) {
/**
* @implements {BrowserProxy}
* @extends {TestBrowserProxy}
*/
let testProxy;
async function createLogo(doodle = null) {
testProxy.handler.setResultFor('getDoodle', Promise.resolve({
doodle: doodle,
}));
const logo = document.createElement('ntp-logo');
document.body.appendChild(logo);
logo.backgroundColor = {value: 0xffffffff};
await flushTasks();
return logo;
}
suiteSetup(() => {
loadTimeData.overrideValues({themeModeDoodlesEnabled});
});
setup(() => {
PolymerTest.clearBody();
testProxy = createTestProxy();
testProxy.handler.setResultFor('onDoodleImageRendered', Promise.resolve({
imageClickParams: '',
interactionLogUrl: null,
shareId: '',
}));
BrowserProxy.instance_ = testProxy;
});
[true, false].forEach(dark => {
const darkStr = dark ? 'dark' : 'light';
test(`setting ${darkStr} simple doodle shows image`, async () => {
// Arrange.
const doodle = createImageDoodle(/*width=*/ 500, /*height=*/ 200);
const imageDoodle = dark ? doodle.image.dark : doodle.image.light;
const shareButton = imageDoodle.shareButton;
// Act.
const logo = await createLogo(doodle);
logo.dark = dark;
logo.backgroundColor = imageDoodle.backgroundColor;
// Assert.
const el = selector => $$(logo, selector);
assertNotStyle(el('#doodle'), 'display', 'none');
assertFalse(!!el('#logo'));
assertEquals(imageDoodle.imageUrl.url, el('#image').src);
assertNotStyle(el('#image'), 'none');
assertEquals(500, el('#image').offsetWidth);
assertEquals(200, el('#image').offsetHeight);
assertNotStyle(el('#shareButton'), 'display', 'none');
assertStyle(
el('#shareButton'), 'background-color',
skColorToRgba(shareButton.backgroundColor));
const buttonPos = getRelativePosition(el('#shareButton'), el('#image'));
assertEquals(shareButton.x, buttonPos.left);
assertEquals(shareButton.y, buttonPos.top);
assertEquals(26, el('#shareButton').offsetWidth);
assertEquals(26, el('#shareButton').offsetHeight);
assertEquals(shareButton.iconUrl.url, el('#shareButtonImage').src);
assertStyle(el('#animation'), 'display', 'none');
assertFalse(!!el('#iframe'));
});
});
[null, '#ff0000'].forEach(color => {
test(`${color || 'no'} background color shows box`, async () => {
// Arrange.
const doodle = createImageDoodle();
doodle.image.light.backgroundColor.value = 0xff0000ff;
// Act.
const logo = await createLogo(doodle);
logo.backgroundColor = !color || hexColorToSkColor(color);
// Assert.
assertStyle($$(logo, '#imageDoodle'), 'padding', '16px 24px');
assertStyle(
$$(logo, '#imageDoodle'), 'background-color', 'rgb(0, 0, 255)');
});
});
[[1000, 500] /* too large */,
[100, 50] /* too small */,
].forEach(([width, height]) => {
test(`${width}x${height} boxed doodle aligned correctly`, async () => {
// Act.
const logo = await createLogo(createImageDoodle(width, height));
logo.dark = true;
logo.backgroundColor = {value: 0xff0000ff};
// Assert.
assertEquals(230, logo.offsetHeight);
assertGE(160, $$(logo, '#image').offsetHeight);
const pos = getRelativePosition($$(logo, '#imageDoodle'), logo);
assertGE(pos.top, 8);
assertEquals(-30, pos.bottom);
});
});
test('dark mode and no dark doodle shows logo', async () => {
// Arrange.
const doodle = createImageDoodle();
delete doodle.image.dark;
// Act.
const logo = await createLogo(doodle);
logo.dark = true;
// Assert.
assertTrue(!!$$(logo, '#doodle'));
assertFalse(!!$$(logo, '#logo'));
});
test('setting too large image doodle resizes image', async () => {
// Arrange.
const doodle = createImageDoodle(/*width=*/ 1000, /*height=*/ 500);
doodle.image.light.shareButton.x = 10;
doodle.image.light.shareButton.y = 20;
// Act.
const logo = await createLogo(doodle);
// Assert.
assertEquals(460, $$(logo, '#image').offsetWidth);
assertEquals(230, $$(logo, '#image').offsetHeight);
const shareButtonPosition =
getRelativePosition($$(logo, '#shareButton'), $$(logo, '#image'));
assertEquals(5, Math.round(shareButtonPosition.left));
assertEquals(9, Math.round(shareButtonPosition.top));
assertEquals(12, $$(logo, '#shareButton').offsetWidth);
assertEquals(12, $$(logo, '#shareButton').offsetHeight);
});
test('setting animated doodle shows image', async () => {
// Arrange.
const doodle = createImageDoodle();
doodle.image.light.imageUrl = {url: 'data:foo'};
doodle.image.light.animationUrl = {url: 'https://foo.com'};
// Act.
const logo = await createLogo(doodle);
// Assert.
assertNotStyle($$(logo, '#doodle'), 'display', 'none');
assertEquals($$(logo, '#logo'), null);
assertEquals($$(logo, '#image').src, 'data:foo');
assertNotStyle($$(logo, '#image'), 'display', 'none');
assertStyle($$(logo, '#animation'), 'display', 'none');
assertFalse(!!$$(logo, '#iframe'));
});
test('setting interactive doodle shows iframe', async () => {
// Act.
const logo = await createLogo({
interactive: {
url: {url: 'https://foo.com'},
width: 200,
height: 100,
}
});
logo.dark = false;
// Assert.
assertNotStyle($$(logo, '#doodle'), 'display', 'none');
assertEquals($$(logo, '#logo'), null);
assertNotStyle($$(logo, '#iframe'), 'display', 'none');
assertStyle($$(logo, '#iframe'), 'width', '200px');
assertStyle($$(logo, '#iframe'), 'height', '100px');
assertStyle($$(logo, '#imageDoodle'), 'display', 'none');
if (themeModeDoodlesEnabled) {
assertEquals(
$$(logo, '#iframe').src, 'https://foo.com/?theme_messages=0');
assertEquals(1, testProxy.getCallCount('postMessage'));
const [iframe, {cmd, dark}, origin] =
await testProxy.whenCalled('postMessage');
assertEquals($$($$(logo, '#iframe'), '#iframe'), iframe);
assertEquals('changeMode', cmd);
assertEquals(false, dark);
assertEquals('https://foo.com', origin);
} else {
assertEquals($$(logo, '#iframe').src, 'https://foo.com/');
}
});
test('message only after mode has been set', async () => {
// Act (no mode).
const logo = await createLogo({
interactive: {
url: {url: 'https://foo.com'},
width: 200,
height: 100,
}
});
// Assert (no mode).
assertEquals(0, testProxy.getCallCount('postMessage'));
// Act (setting mode).
logo.dark = true;
// Assert (setting mode).
if (themeModeDoodlesEnabled) {
assertEquals(1, testProxy.getCallCount('postMessage'));
const [iframe, {cmd, dark}, origin] =
await testProxy.whenCalled('postMessage');
assertEquals($$($$(logo, '#iframe'), '#iframe'), iframe);
assertEquals('changeMode', cmd);
assertEquals(true, dark);
assertEquals('https://foo.com', origin);
} else {
assertEquals(0, testProxy.getCallCount('postMessage'));
}
});
test('disallowing doodle shows logo', async () => {
// Act.
const logo = await createLogo(createImageDoodle());
logo.doodleAllowed = false;
Array.from(logo.shadowRoot.querySelectorAll('dom-if')).forEach((domIf) => {
domIf.render();
});
// Assert.
assertNotStyle($$(logo, '#logo'), 'display', 'none');
assertEquals($$(logo, '#doodle'), null);
});
test('before doodle loaded shows nothing', () => {
// Act.
testProxy.handler.setResultFor('getDoodle', new Promise(() => {}));
const logo = document.createElement('ntp-logo');
document.body.appendChild(logo);
// Assert.
assertEquals($$(logo, '#logo'), null);
assertEquals($$(logo, '#doodle'), null);
});
test('unavailable doodle shows logo', async () => {
// Act.
const logo = await createLogo();
// Assert.
assertNotStyle($$(logo, '#logo'), 'display', 'none');
assertEquals($$(logo, '#doodle'), null);
});
test('not setting-single colored shows multi-colored logo', async () => {
// Act.
const logo = await createLogo();
// Assert.
assertNotStyle($$(logo, '#logo'), 'background-image', '');
assertStyle($$(logo, '#logo'), '-webkit-mask-image', 'none');
assertStyle($$(logo, '#logo'), 'background-color', 'rgba(0, 0, 0, 0)');
});
test('setting single-colored shows single-colored logo', async () => {
// Act.
const logo = await createLogo();
logo.singleColored = true;
logo.style.setProperty('--ntp-logo-color', 'red');
// Assert.
assertNotStyle($$(logo, '#logo'), '-webkit-mask-image', 'none');
assertStyle($$(logo, '#logo'), 'background-color', 'rgb(255, 0, 0)');
assertStyle($$(logo, '#logo'), 'background-image', 'none');
});
test('logo aligned correctly', async () => {
// Act.
const logo = await createLogo();
// Assert.
const pos = getRelativePosition($$(logo, '#logo'), logo);
assertEquals(108, pos.top);
assertEquals(92, $$(logo, '#logo').offsetHeight);
});
test('doodle aligned correctly', async () => {
// Act.
const logo = await createLogo(createImageDoodle());
// Assert.
const pos = getRelativePosition($$(logo, '#doodle'), logo);
assertEquals(0, pos.top);
});
// Disabled for flakiness, see https://crbug.com/1065812.
test.skip('receiving resize message resizes doodle', async () => {
// Arrange.
const logo =
await createLogo({interactive: {url: {url: 'https://foo.com'}}});
const transitionend = eventToPromise('transitionend', $$(logo, '#iframe'));
// Act.
window.postMessage(
{
cmd: 'resizeDoodle',
duration: '500ms',
height: '500px',
width: '700px',
},
'*');
await transitionend;
// Assert.
const transitionedProperties = window.getComputedStyle($$(logo, '#iframe'))
.getPropertyValue('transition-property')
.trim()
.split(',')
.map(s => s.trim());
assertStyle($$(logo, '#iframe'), 'transition-duration', '0.5s');
assertTrue(transitionedProperties.includes('height'));
assertTrue(transitionedProperties.includes('width'));
assertEquals($$(logo, '#iframe').offsetHeight, 500);
assertEquals($$(logo, '#iframe').offsetWidth, 700);
assertGE(logo.offsetHeight, 500);
assertGE(logo.offsetWidth, 700);
});
test('receiving other message does not resize doodle', async () => {
// Arrange.
const logo =
await createLogo({interactive: {url: {url: 'https://foo.com'}}});
const height = $$(logo, '#iframe').offsetHeight;
const width = $$(logo, '#iframe').offsetWidth;
// Act.
window.postMessage(
{
cmd: 'foo',
duration: '500ms',
height: '500px',
width: '700px',
},
'*');
await flushTasks();
// Assert.
assertEquals($$(logo, '#iframe').offsetHeight, height);
assertEquals($$(logo, '#iframe').offsetWidth, width);
});
test('receiving mode message sends mode', async () => {
// Arrange.
const logo =
await createLogo({interactive: {url: {url: 'https://foo.com'}}});
logo.dark = false;
testProxy.resetResolver('postMessage');
// Act.
window.postMessage({cmd: 'sendMode'}, '*');
await flushTasks();
// Assert.
if (themeModeDoodlesEnabled) {
assertEquals(1, testProxy.getCallCount('postMessage'));
const [_, {cmd, dark}, origin] =
await testProxy.whenCalled('postMessage');
assertEquals('changeMode', cmd);
assertEquals(false, dark);
assertEquals('https://foo.com', origin);
} else {
assertEquals(0, testProxy.getCallCount('postMessage'));
}
});
test('clicking simple doodle opens link', async () => {
// Arrange.
const doodle = createImageDoodle();
doodle.image.onClickUrl = {url: 'https://foo.com'};
const logo = await createLogo(doodle);
// Act.
$$(logo, '#image').click();
const url = await testProxy.whenCalled('open');
// Assert.
assertEquals(url, 'https://foo.com/');
});
[' ', 'Enter'].forEach(key => {
test(`pressing ${key} on simple doodle opens link`, async () => {
// Arrange.
const doodle = createImageDoodle();
doodle.image.onClickUrl = {url: 'https://foo.com'};
const logo = await createLogo(doodle);
// Act.
keydown($$(logo, '#image'), key);
const url = await testProxy.whenCalled('open');
// Assert.
assertEquals(url, 'https://foo.com/');
});
});
test('animated doodle starts and stops', async () => {
// Arrange.
const doodle = createImageDoodle();
doodle.image.light.animationUrl = {url: 'https://foo.com'};
const logo = await createLogo(doodle);
// Act (start animation).
$$(logo, '#image').click();
// Assert (animation started).
assertEquals(testProxy.getCallCount('open'), 0);
assertNotStyle($$(logo, '#image'), 'display', 'none');
assertNotStyle($$(logo, '#animation'), 'display', 'none');
assertEquals(
$$(logo, '#animation').src,
'chrome-untrusted://new-tab-page/image?https://foo.com');
assertDeepEquals(
$$(logo, '#image').getBoundingClientRect(),
$$(logo, '#animation').getBoundingClientRect());
// Act (switch mode).
logo.dark = true;
// Assert (animation stopped).
assertNotStyle($$(logo, '#image'), 'display', 'none');
assertStyle($$(logo, '#animation'), 'display', 'none');
});
test('clicking animation of animated doodle opens link', async () => {
// Arrange.
const doodle = createImageDoodle();
doodle.image.light.animationUrl = {url: 'https://foo.com'};
doodle.image.onClickUrl = {url: 'https://bar.com'};
const logo = await createLogo(doodle);
$$(logo, '#image').click();
// Act.
$$(logo, '#animation').click();
const url = await testProxy.whenCalled('open');
// Assert.
assertEquals(url, 'https://bar.com/');
});
test('share dialog removed on start', async () => {
// Arrange.
const logo = await createLogo(createImageDoodle());
// Assert.
assertFalse(!!logo.shadowRoot.querySelector('ntp-doodle-share-dialog'));
});
test('clicking share button adds share dialog', async () => {
// Arrange.
const logo = await createLogo(createImageDoodle());
// Act.
$$(logo, '#shareButton').click();
await flushTasks();
// Assert.
assertTrue(!!logo.shadowRoot.querySelector('ntp-doodle-share-dialog'));
});
test('closing share dialog removes share dialog', async () => {
// Arrange.
const logo = await createLogo(createImageDoodle());
$$(logo, '#shareButton').click();
await flushTasks();
// Act.
logo.shadowRoot.querySelector('ntp-doodle-share-dialog')
.dispatchEvent(new Event('close'));
await flushTasks();
// Assert.
assertFalse(!!logo.shadowRoot.querySelector('ntp-doodle-share-dialog'));
});
[true, false].forEach(dark => {
const darkStr = dark ? 'dark' : 'light';
test(`${darkStr} simple doodle logging flow`, async () => {
// Arrange.
const doodleResolver = new PromiseResolver();
testProxy.handler.setResultFor('getDoodle', doodleResolver.promise);
const logo = document.createElement('ntp-logo');
document.body.appendChild(logo);
logo.dark = dark;
testProxy.handler.setResultFor('onDoodleImageRendered', Promise.resolve({
imageClickParams: 'foo=bar&hello=world',
interactionLogUrl: null,
shareId: '123',
}));
const doodle = createImageDoodle();
doodle.image.onClickUrl = {url: 'https://click.com?ct=supi'};
const imageDoodle = dark ? doodle.image.dark : doodle.image.light;
// Act (load).
doodleResolver.resolve({doodle});
await flushTasks();
// Assert (load).
const [type, _, logUrl] =
await testProxy.handler.whenCalled('onDoodleImageRendered');
assertEquals(newTabPage.mojom.DoodleImageType.STATIC, type);
assertEquals(imageDoodle.imageImpressionLogUrl.url, logUrl.url);
// Act (click).
$$(logo, '#image').click();
// Assert (click).
const [type2] =
await testProxy.handler.whenCalled('onDoodleImageClicked');
const onClickUrl = await testProxy.whenCalled('open');
assertEquals(newTabPage.mojom.DoodleImageType.STATIC, type2);
assertEquals(
'https://click.com/?ct=supi&foo=bar&hello=world', onClickUrl);
// Act (share).
$$(logo, '#shareButton').click();
await flushTasks();
$$(logo, 'ntp-doodle-share-dialog')
.dispatchEvent(new CustomEvent(
'share', {detail: newTabPage.mojom.DoodleShareChannel.FACEBOOK}));
// Assert (share).
const [channel, doodleId, shareId] =
await testProxy.handler.whenCalled('onDoodleShared');
assertEquals(newTabPage.mojom.DoodleShareChannel.FACEBOOK, channel);
assertEquals('supi', doodleId);
assertEquals('123', shareId);
});
test(`${darkStr} animated doodle logging flow`, async () => {
// Arrange.
const doodleResolver = new PromiseResolver();
testProxy.handler.setResultFor('getDoodle', doodleResolver.promise);
const logo = document.createElement('ntp-logo');
document.body.appendChild(logo);
logo.dark = dark;
testProxy.handler.setResultFor('onDoodleImageRendered', Promise.resolve({
imageClickParams: '',
interactionLogUrl: {url: 'https://interaction.com'},
shareId: '',
}));
const doodle = createImageDoodle();
doodle.image.onClickUrl = {url: 'https://click.com?ct=supi'};
doodle.image.light.animationUrl = {url: 'https://animation.com'};
doodle.image.dark.animationUrl = {url: 'https://dark_animation.com'};
doodle.image.light.animationImpressionLogUrl = {
url: 'https://animation_log.com'
};
doodle.image.dark.animationImpressionLogUrl = {
url: 'https://dark_animation_log.com'
};
const imageDoodle = dark ? doodle.image.dark : doodle.image.light;
// Act (CTA load).
doodleResolver.resolve({doodle});
await flushTasks();
// Assert (CTA load).
const [type, _, logUrl] =
await testProxy.handler.whenCalled('onDoodleImageRendered');
assertEquals(newTabPage.mojom.DoodleImageType.CTA, type);
assertEquals(imageDoodle.imageImpressionLogUrl.url, logUrl.url);
// Act (CTA click).
testProxy.handler.resetResolver('onDoodleImageRendered');
testProxy.handler.setResultFor('onDoodleImageRendered', Promise.resolve({
imageClickParams: 'foo=bar&hello=world',
interactionLogUrl: null,
shareId: '123',
}));
$$(logo, '#image').click();
// Assert (CTA click).
const [type2, interactionLogUrl] =
await testProxy.handler.whenCalled('onDoodleImageClicked');
assertEquals(newTabPage.mojom.DoodleImageType.CTA, type2);
assertEquals('https://interaction.com', interactionLogUrl.url);
// Assert (animation load). Also triggered by clicking #image.
const [type3, __, logUrl2] =
await testProxy.handler.whenCalled('onDoodleImageRendered');
assertEquals(newTabPage.mojom.DoodleImageType.ANIMATION, type3);
assertEquals(imageDoodle.animationImpressionLogUrl.url, logUrl2.url);
// Act (animation click).
testProxy.handler.resetResolver('onDoodleImageClicked');
$$(logo, '#animation').click();
// Assert (animation click).
const [type4, ___] =
await testProxy.handler.whenCalled('onDoodleImageClicked');
const onClickUrl = await testProxy.whenCalled('open');
assertEquals(newTabPage.mojom.DoodleImageType.ANIMATION, type4);
assertEquals(
'https://click.com/?ct=supi&foo=bar&hello=world', onClickUrl);
// Act (share).
$$(logo, '#shareButton').click();
await flushTasks();
$$(logo, 'ntp-doodle-share-dialog')
.dispatchEvent(new CustomEvent(
'share', {detail: newTabPage.mojom.DoodleShareChannel.TWITTER}));
// Assert (share).
const [channel, doodleId, shareId] =
await testProxy.handler.whenCalled('onDoodleShared');
assertEquals(newTabPage.mojom.DoodleShareChannel.TWITTER, channel);
assertEquals('supi', doodleId);
assertEquals('123', shareId);
});
});
}
suite('NewTabPageLogoTest', () => {
[true, false].forEach(themeModeDoodlesEnabled => {
const enabled = themeModeDoodlesEnabled ? 'enabled' : 'disabled';
suite(`theme mode doodles ${enabled}`, () => {
createSuite(themeModeDoodlesEnabled);
});
});
});