Commit a8e39e68 authored by John Lee's avatar John Lee Committed by Commit Bot

Tab Strip WebUI: Display alert state indicators for tabs

- Add |alertStates| as property on |TabData|, sent from C++
- Move fade in and fade out animations out from CSS to JS to allow
wrapping them within Promises, so that TabElement can reliably tell
when there are AlertIndicator elements in the DOM.
- Fix tests such that SVGs load from chrome://tab-strip instead of
chrome://test.

Screencast in attached bug.

Bug: 1004983
Change-Id: I32cb6c91169646c244070826875a8317b8c9b296
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1861124Reviewed-by: default avatarDemetrios Papadopoulos <dpapad@chromium.org>
Commit-Queue: John Lee <johntlee@chromium.org>
Cr-Commit-Position: refs/heads/master@{#707217}
parent 2509e355
...@@ -8,6 +8,7 @@ import("//tools/polymer/polymer.gni") ...@@ -8,6 +8,7 @@ import("//tools/polymer/polymer.gni")
js_type_check("closure_compile") { js_type_check("closure_compile") {
deps = [ deps = [
":alert_indicator", ":alert_indicator",
":alert_indicators",
":custom_element", ":custom_element",
":tab", ":tab",
":tab_list", ":tab_list",
...@@ -19,6 +20,9 @@ js_type_check("closure_compile") { ...@@ -19,6 +20,9 @@ js_type_check("closure_compile") {
js_library("alert_indicator") { js_library("alert_indicator") {
} }
js_library("alert_indicators") {
}
js_library("custom_element") { js_library("custom_element") {
} }
...@@ -47,6 +51,7 @@ js_library("tab_strip_embedder_proxy") { ...@@ -47,6 +51,7 @@ js_library("tab_strip_embedder_proxy") {
group("tab_strip_modules") { group("tab_strip_modules") {
deps = [ deps = [
":alert_indicator_module", ":alert_indicator_module",
":alert_indicators_module",
":tab_list_module", ":tab_list_module",
":tab_module", ":tab_module",
] ]
...@@ -58,6 +63,12 @@ polymer_modulizer("alert_indicator") { ...@@ -58,6 +63,12 @@ polymer_modulizer("alert_indicator") {
html_type = "v3-ready" html_type = "v3-ready"
} }
polymer_modulizer("alert_indicators") {
js_file = "alert_indicators.js"
html_file = "alert_indicators.html"
html_type = "v3-ready"
}
polymer_modulizer("tab") { polymer_modulizer("tab") {
js_file = "tab.js" js_file = "tab.js"
html_file = "tab.html" html_file = "tab.html"
......
<style> <style>
@keyframes fade-in {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes fade-out {
0% { opacity: 1; }
100% { opacity: 0; }
}
:host { :host {
-webkit-mask: center / contain no-repeat; -webkit-mask: center / contain no-repeat;
animation: fade-in 200ms ease-in forwards;
background-color: currentColor; background-color: currentColor;
display: block; display: block;
height: 100%; height: 100%;
opacity: 0; overflow: hidden;
width: 16px; width: 16px;
} }
:host([fade-out]) { :host([alert-state_='0']),
animation: fade-out 1000ms ease-in; :host([alert-state_='8']) {
-webkit-mask-image: url(alert_indicators/tab_media_recording.svg);
background-color: var(--tabstrip-indicator-recording-color);
} }
:host([alert-state='pip']) { :host([alert-state_='1']) {
-webkit-mask-image: url(alert_indicators/picture_in_picture_alt.svg); -webkit-mask-image:
background-color: var(--tabstrip-indicator-pip-color); url(alert_indicators/tab_media_capturing_with_arrow.svg);
background-color: var(--tabstrip-indicator-capturing-color);
} }
:host([alert-state='serial']) { :host([alert-state_='2']) {
-webkit-mask-image: url(alert_indicators/serial_port.svg); -webkit-mask-image: url(alert_indicators/tab_audio_rounded.svg);
} }
:host([alert-state='muted']) { :host([alert-state_='3']) {
-webkit-mask-image: url(alert_indicators/tab_audio_muting_rounded.svg); -webkit-mask-image: url(alert_indicators/tab_audio_muting_rounded.svg);
} }
:host([alert-state='audio']) { :host([alert-state_='4']) {
-webkit-mask-image: url(alert_indicators/tab_audio_rounded.svg);
}
:host([alert-state='bluetooth']) {
-webkit-mask-image: url(alert_indicators/tab_bluetooth_connected.svg); -webkit-mask-image: url(alert_indicators/tab_bluetooth_connected.svg);
} }
:host([alert-state='capturing']) { :host([alert-state_='5']) {
-webkit-mask-image: -webkit-mask-image: url(alert_indicators/tab_usb_connected.svg);
url(alert_indicators/tab_media_capturing_with_arrow.svg); -webkit-mask-size: 20px;
background-color: var(--tabstrip-indicator-capturing-color);
} }
:host([alert-state='recording']) { :host([alert-state_='6']) {
-webkit-mask-image: url(alert_indicators/tab_media_recording.svg); -webkit-mask-image: url(alert_indicators/serial_port.svg);
background-color: var(--tabstrip-indicator-recording-color);
} }
:host([alert-state='usb']) { :host([alert-state_='7']) {
-webkit-mask-image: url(alert_indicators/tab_usb_connected.svg); -webkit-mask-image: url(alert_indicators/picture_in_picture_alt.svg);
-webkit-mask-size: 20px; background-color: var(--tabstrip-indicator-pip-color);
} }
:host([alert-state='vr']) { :host([alert-state_='9']) {
-webkit-mask-image: url(alert_indicators/vr_headset.svg); -webkit-mask-image: url(alert_indicators/vr_headset.svg);
} }
:host([alert-state='capturing']),
:host([alert-state='recording']) {
animation:
fade-in 200ms ease-in 0,
fade-out 1000ms ease-in 200ms,
fade-in 200ms ease-in 1200s,
fade-out 1000ms ease-in 1400ms,
fade-in 200ms ease-in 2400ms;
animation-fill-mode: forwards;
}
</style> </style>
...@@ -3,16 +3,145 @@ ...@@ -3,16 +3,145 @@
// found in the LICENSE file. // found in the LICENSE file.
import {CustomElement} from './custom_element.js'; import {CustomElement} from './custom_element.js';
import {TabAlertState} from './tabs_api_proxy.js';
/** @const {string} */
const MAX_WIDTH = '16px';
export class AlertIndicatorElement extends CustomElement { export class AlertIndicatorElement extends CustomElement {
static get template() { static get template() {
return `{__html_template__}`; return `{__html_template__}`;
} }
/** @override */ constructor() {
remove() { super();
this.toggleAttribute('fade-out', true);
this.addEventListener('animationend', () => super.remove(), {once: true}); /** @private {!TabAlertState} */
this.alertState_;
/** @private {number} */
this.fadeDurationMs_ = 125;
/**
* An animation that is currently in-flight to fade the element in.
* @private {?Animation}
*/
this.fadeInAnimation_ = null;
/**
* An animation that is currently in-flight to fade the element out.
* @private {?Animation}
*/
this.fadeOutAnimation_ = null;
/**
* A promise that resolves when the fade out animation finishes or rejects
* if a fade out animation is canceled.
* @private {?Promise}
*/
this.fadeOutAnimationPromise_ = null;
}
/** @return {!TabAlertState} */
get alertState() {
return this.alertState_;
}
/** @param {!TabAlertState} alertState */
set alertState(alertState) {
this.setAttribute('alert-state_', alertState);
this.alertState_ = alertState;
}
/** @param {number} duration */
overrideFadeDurationForTesting(duration) {
this.fadeDurationMs_ = duration;
}
show() {
if (this.fadeOutAnimation_) {
// Cancel any fade out animations to prevent the element from fading out
// and being removed. At this point, the tab's alertStates have changed
// to a state in which this indicator should be visible.
this.fadeOutAnimation_.cancel();
this.fadeOutAnimation_ = null;
this.fadeOutAnimationPromise_ = null;
}
if (this.fadeInAnimation_) {
// If the element was already faded in, don't fade it in again
return;
}
if (this.alertState_ === TabAlertState.MEDIA_RECORDING ||
this.alertState_ === TabAlertState.TAB_CAPTURING ||
this.alertState_ === TabAlertState.DESKTOP_CAPTURING) {
// Fade in and out 2 times and then fade in
const totalDuration = 2600;
this.fadeInAnimation_ = this.animate(
[
{opacity: 0, maxWidth: 0, offset: 0},
{opacity: 1, maxWidth: MAX_WIDTH, offset: 200 / totalDuration},
{opacity: 0, maxWidth: MAX_WIDTH, offset: 1200 / totalDuration},
{opacity: 1, maxWidth: MAX_WIDTH, offset: 1400 / totalDuration},
{opacity: 0, maxWidth: MAX_WIDTH, offset: 2400 / totalDuration},
{opacity: 1, maxWidth: MAX_WIDTH, offset: 1},
],
{
duration: totalDuration,
easing: 'linear',
fill: 'forwards',
});
} else {
this.fadeInAnimation_ = this.animate(
[
{opacity: 0, maxWidth: 0},
{opacity: 1, maxWidth: MAX_WIDTH},
],
{
duration: this.fadeDurationMs_,
fill: 'forwards',
});
}
}
/** @return {!Promise} */
hide() {
if (this.fadeInAnimation_) {
// Cancel any fade in animations to prevent the element from fading in. At
// this point, the tab's alertStates have changed to a state in which this
// indicator should not be visible.
this.fadeInAnimation_.cancel();
this.fadeInAnimation_ = null;
}
if (this.fadeOutAnimationPromise_) {
return this.fadeOutAnimationPromise_;
}
this.fadeOutAnimationPromise_ = new Promise((resolve, reject) => {
this.fadeOutAnimation_ = this.animate(
[
{opacity: 1, maxWidth: MAX_WIDTH},
{opacity: 0, maxWidth: 0},
],
{
duration: this.fadeDurationMs_,
fill: 'forwards',
});
this.fadeOutAnimation_.addEventListener('cancel', () => {
reject();
});
this.fadeOutAnimation_.addEventListener('finish', () => {
this.remove();
this.fadeOutAnimation_ = null;
this.fadeOutAnimationPromise_ = null;
resolve();
});
});
return this.fadeOutAnimationPromise_;
} }
} }
......
<style>
:host {
height: 100%;
}
#container {
display: grid;
flex-shrink: 0;
grid-auto-flow: column;
grid-gap: 4px;
height: 100%;
}
/* Pinned tabs only show the first indicator. */
:host-context([pinned]) tabstrip-alert-indicator:not(:first-child) {
display: none;
}
</style>
<div id="container"></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 {AlertIndicatorElement} from './alert_indicator.js';
import {CustomElement} from './custom_element.js';
import {TabAlertState} from './tabs_api_proxy.js';
export class AlertIndicatorsElement extends CustomElement {
static get template() {
return `{__html_template__}`;
}
constructor() {
super();
/** @private {!HTMLElement} */
this.containerEl_ = /** @type {!HTMLElement} */ (
this.shadowRoot.querySelector('#container'));
const audioIndicator = new AlertIndicatorElement();
const recordingIndicator = new AlertIndicatorElement();
/** @private {!Map<!TabAlertState, !AlertIndicatorElement>} */
this.alertIndicators_ = new Map([
[TabAlertState.MEDIA_RECORDING, recordingIndicator],
[TabAlertState.TAB_CAPTURING, new AlertIndicatorElement()],
[TabAlertState.AUDIO_PLAYING, audioIndicator],
[TabAlertState.AUDIO_MUTING, audioIndicator],
[TabAlertState.BLUETOOTH_CONNECTED, new AlertIndicatorElement()],
[TabAlertState.USB_CONNECTED, new AlertIndicatorElement()],
[TabAlertState.SERIAL_CONNECTED, new AlertIndicatorElement()],
[TabAlertState.PIP_PLAYING, new AlertIndicatorElement()],
[TabAlertState.DESKTOP_CAPTURING, recordingIndicator],
[TabAlertState.VR_PRESENTING_IN_HEADSET, new AlertIndicatorElement()],
]);
}
/**
* @param {!Array<!TabAlertState>} alertStates
* @return {!Promise<number>} A promise that resolves with the number of
* AlertIndicatorElements that are currently visible.
*/
updateAlertStates(alertStates) {
const alertIndicators =
alertStates.map(alertState => this.alertIndicators_.get(alertState));
let alertIndicatorCount = 0;
for (const [index, alertState] of alertStates.entries()) {
const alertIndicator = alertIndicators[index];
// Don't show unsupported indicators.
if (!alertIndicator) {
continue;
}
// If the same indicator appears earlier in the list of alert indicators,
// that indicates that there is a higher priority alert state that
// should display for the shared indicator.
if (alertIndicators.indexOf(alertIndicator) < index) {
continue;
}
// Always update alert state to ensure the correct icon is displayed.
alertIndicator.alertState = alertState;
this.containerEl_.insertBefore(
alertIndicator, this.containerEl_.children[alertIndicatorCount]);
// Only fade in if this is just being added to the DOM.
alertIndicator.show();
alertIndicatorCount++;
}
const animationPromises = Array.from(this.containerEl_.children)
.slice(alertIndicatorCount)
.map(indicator => indicator.hide());
return Promise.all(animationPromises)
.then(
() => {
return this.containerEl_.childElementCount;
},
() => {
// A failure in the animation promises means an animation was
// canceled and therefore there is a new set of alertStates
// being animated.
});
}
}
customElements.define('tabstrip-alert-indicators', AlertIndicatorsElement);
...@@ -66,7 +66,7 @@ ...@@ -66,7 +66,7 @@
opacity 0ms; opacity 0ms;
} }
:host([pinned_]) #faviconContainer { :host([pinned]) #faviconContainer {
margin: 0; margin: 0;
} }
...@@ -168,6 +168,7 @@ ...@@ -168,6 +168,7 @@
} }
#titleText { #titleText {
flex: 1;
font-size: 100%; font-size: 100%;
font-weight: normal; font-weight: normal;
overflow: hidden; overflow: hidden;
...@@ -175,6 +176,10 @@ ...@@ -175,6 +176,10 @@
white-space: nowrap; white-space: nowrap;
} }
:host([has-alert-states_]) #titleText {
margin-inline-end: 4px;
}
#close { #close {
-webkit-appearance: none; -webkit-appearance: none;
align-items: center; align-items: center;
...@@ -186,7 +191,6 @@ ...@@ -186,7 +191,6 @@
flex-shrink: 0; flex-shrink: 0;
height: 100%; height: 100%;
justify-content: center; justify-content: center;
margin-inline-start: auto;
padding: 0; padding: 0;
position: relative; position: relative;
width: 36px; width: 36px;
...@@ -224,19 +228,23 @@ ...@@ -224,19 +228,23 @@
} }
/* Pinned tab styles */ /* Pinned tab styles */
:host([pinned_]) { :host([pinned]) {
height: var(--tabstrip-pinned-tab-size); height: var(--tabstrip-pinned-tab-size);
width: var(--tabstrip-pinned-tab-size); width: var(--tabstrip-pinned-tab-size);
} }
:host([pinned_]) #title { :host([pinned]) #title {
border-block-end: 0; border-block-end: 0;
height: 100%; height: 100%;
} }
:host([pinned_]) #titleText, :host([pinned]) #titleText,
:host([pinned_]) #close, :host([pinned]) #close,
:host([pinned_]) #thumbnail { :host([pinned]) #thumbnail {
display: none;
}
:host([pinned][has-alert-states_]) #faviconContainer {
display: none; display: none;
} }
...@@ -268,6 +276,7 @@ ...@@ -268,6 +276,7 @@
<div id="blocked"></div> <div id="blocked"></div>
</div> </div>
<h2 id="titleText"></h2> <h2 id="titleText"></h2>
<tabstrip-alert-indicators></tabstrip-alert-indicators>
<button id="close"> <button id="close">
<span id="closeIcon"></span> <span id="closeIcon"></span>
</button> </button>
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import {assert} from 'chrome://resources/js/assert.m.js'; import {assert} from 'chrome://resources/js/assert.m.js';
import {getFavicon} from 'chrome://resources/js/icon.m.js'; import {getFavicon} from 'chrome://resources/js/icon.m.js';
import {AlertIndicatorsElement} from './alert_indicators.js';
import {CustomElement} from './custom_element.js'; import {CustomElement} from './custom_element.js';
import {TabStripEmbedderProxy} from './tab_strip_embedder_proxy.js'; import {TabStripEmbedderProxy} from './tab_strip_embedder_proxy.js';
import {TabData, TabNetworkState, TabsApiProxy} from './tabs_api_proxy.js'; import {TabData, TabNetworkState, TabsApiProxy} from './tabs_api_proxy.js';
...@@ -19,6 +20,13 @@ export class TabElement extends CustomElement { ...@@ -19,6 +20,13 @@ export class TabElement extends CustomElement {
constructor() { constructor() {
super(); super();
this.alertIndicatorsEl_ = /** @type {!AlertIndicatorsElement} */
(this.shadowRoot.querySelector('tabstrip-alert-indicators'));
// Normally, custom elements will get upgraded automatically once added to
// the DOM, but TabElement may need to update properties on
// AlertIndicatorElement before this happens, so upgrade it manually.
customElements.upgrade(this.alertIndicatorsEl_);
/** @private {!HTMLElement} */ /** @private {!HTMLElement} */
this.closeButtonEl_ = this.closeButtonEl_ =
/** @type {!HTMLElement} */ (this.shadowRoot.querySelector('#close')); /** @type {!HTMLElement} */ (this.shadowRoot.querySelector('#close'));
...@@ -77,7 +85,7 @@ export class TabElement extends CustomElement { ...@@ -77,7 +85,7 @@ export class TabElement extends CustomElement {
'loading_', 'loading_',
!tab.shouldHideThrobber && !tab.shouldHideThrobber &&
tab.networkState === TabNetworkState.LOADING); tab.networkState === TabNetworkState.LOADING);
this.toggleAttribute('pinned_', tab.pinned); this.toggleAttribute('pinned', tab.pinned);
this.toggleAttribute('blocked_', tab.blocked); this.toggleAttribute('blocked_', tab.blocked);
this.setAttribute('draggable', tab.pinned); this.setAttribute('draggable', tab.pinned);
this.toggleAttribute('crashed_', tab.crashed); this.toggleAttribute('crashed_', tab.crashed);
...@@ -99,6 +107,11 @@ export class TabElement extends CustomElement { ...@@ -99,6 +107,11 @@ export class TabElement extends CustomElement {
// Expose the ID to an attribute to allow easy querySelector use // Expose the ID to an attribute to allow easy querySelector use
this.setAttribute('data-tab-id', tab.id); this.setAttribute('data-tab-id', tab.id);
this.alertIndicatorsEl_.updateAlertStates(tab.alertStates)
.then((alertIndicatorsCount) => {
this.toggleAttribute('has-alert-states_', alertIndicatorsCount > 0);
});
if (!this.tab_ || this.tab_.id !== tab.id) { if (!this.tab_ || this.tab_.id !== tab.id) {
this.tabsApi_.trackThumbnailForTab(tab.id); this.tabsApi_.trackThumbnailForTab(tab.id);
} }
......
...@@ -45,6 +45,12 @@ ...@@ -45,6 +45,12 @@
use_base_dir="false" use_base_dir="false"
type="chrome_html" type="chrome_html"
compress="gzip"/> compress="gzip"/>
<structure
name="IDR_TAB_STRIP_ALERT_INDICATORS_JS"
file="${root_gen_dir}/chrome/browser/resources/tab_strip/alert_indicators.js"
use_base_dir="false"
type="chrome_html"
compress="gzip"/>
<structure <structure
name="IDR_TAB_STRIP_EMBEDDER_PROXY_JS" name="IDR_TAB_STRIP_EMBEDDER_PROXY_JS"
file="tab_strip_embedder_proxy.js" file="tab_strip_embedder_proxy.js"
......
...@@ -16,9 +16,28 @@ export const TabNetworkState = { ...@@ -16,9 +16,28 @@ export const TabNetworkState = {
ERROR: 3, ERROR: 3,
}; };
/**
* Must be kept in sync with TabAlertState from
* //chrome/browser ui/tabs/tab_utils.h
* @enum {number}
*/
export const TabAlertState = {
MEDIA_RECORDING: 0,
TAB_CAPTURING: 1,
AUDIO_PLAYING: 2,
AUDIO_MUTING: 3,
BLUETOOTH_CONNECTED: 4,
USB_CONNECTED: 5,
SERIAL_CONNECTED: 6,
PIP_PLAYING: 7,
DESKTOP_CAPTURING: 8,
VR_PRESENTING_IN_HEADSET: 9,
};
/** /**
* @typedef {{ * @typedef {{
* active: boolean, * active: boolean,
* alertStates: !Array<!TabAlertState>,
* blocked: boolean, * blocked: boolean,
* crashed: boolean, * crashed: boolean,
* favIconUrl: (string|undefined), * favIconUrl: (string|undefined),
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
#include "chrome/browser/ui/tabs/tab_renderer_data.h" #include "chrome/browser/ui/tabs/tab_renderer_data.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h" #include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/browser/ui/tabs/tab_utils.h"
#include "chrome/browser/ui/webui/favicon_source.h" #include "chrome/browser/ui/webui/favicon_source.h"
#include "chrome/browser/ui/webui/theme_handler.h" #include "chrome/browser/ui/webui/theme_handler.h"
#include "chrome/common/webui_url_constants.h" #include "chrome/common/webui_url_constants.h"
...@@ -42,6 +43,7 @@ ...@@ -42,6 +43,7 @@
#include "ui/base/theme_provider.h" #include "ui/base/theme_provider.h"
#include "ui/gfx/color_utils.h" #include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/point_conversions.h" #include "ui/gfx/geometry/point_conversions.h"
#include "ui/resources/grit/webui_resources.h"
namespace { namespace {
...@@ -247,6 +249,13 @@ class TabStripUIHandler : public content::WebUIMessageHandler, ...@@ -247,6 +249,13 @@ class TabStripUIHandler : public content::WebUIMessageHandler,
tab_data.SetBoolean("crashed", tab_renderer_data.IsCrashed()); tab_data.SetBoolean("crashed", tab_renderer_data.IsCrashed());
// TODO(johntlee): Add the rest of TabRendererData // TODO(johntlee): Add the rest of TabRendererData
auto alert_states = std::make_unique<base::ListValue>();
for (const auto alert_state :
chrome::GetTabAlertStatesForContents(contents)) {
alert_states->Append(static_cast<int>(alert_state));
}
tab_data.SetList("alertStates", std::move(alert_states));
return tab_data; return tab_data;
} }
...@@ -396,6 +405,9 @@ TabStripUI::TabStripUI(content::WebUI* web_ui) ...@@ -396,6 +405,9 @@ TabStripUI::TabStripUI(content::WebUI* web_ui)
content::WebUIDataSource* html_source = content::WebUIDataSource* html_source =
content::WebUIDataSource::Create(chrome::kChromeUITabStripHost); content::WebUIDataSource::Create(chrome::kChromeUITabStripHost);
html_source->OverrideContentSecurityPolicyScriptSrc(
"script-src chrome://resources chrome://test 'self';");
std::string generated_path = std::string generated_path =
"@out_folder@/gen/chrome/browser/resources/tab_strip/"; "@out_folder@/gen/chrome/browser/resources/tab_strip/";
...@@ -406,6 +418,8 @@ TabStripUI::TabStripUI(content::WebUI* web_ui) ...@@ -406,6 +418,8 @@ TabStripUI::TabStripUI(content::WebUI* web_ui)
} }
html_source->AddResourcePath(path, kTabStripResources[i].value); html_source->AddResourcePath(path, kTabStripResources[i].value);
} }
html_source->AddResourcePath("test_loader.js", IDR_WEBUI_JS_TEST_LOADER);
html_source->AddResourcePath("test_loader.html", IDR_WEBUI_HTML_TEST_LOADER);
html_source->SetDefaultResource(IDR_TAB_STRIP_HTML); html_source->SetDefaultResource(IDR_TAB_STRIP_HTML);
......
// 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://tab-strip/alert_indicator.js';
suite('AlertIndicator', () => {
let alertIndicatorElement;
let alertIndicatorStyle;
setup(() => {
document.body.innerHTML = '';
alertIndicatorElement = document.createElement('tabstrip-alert-indicator');
document.body.appendChild(alertIndicatorElement);
alertIndicatorStyle = window.getComputedStyle(alertIndicatorElement);
});
test('fades in on show', () => {
alertIndicatorElement.overrideFadeDurationForTesting(0);
alertIndicatorElement.show();
assertEquals(alertIndicatorStyle.opacity, '1');
assertEquals(alertIndicatorStyle.maxWidth, '16px');
});
test('fades out on hide', async () => {
alertIndicatorElement.overrideFadeDurationForTesting(0);
const hideAnimation = alertIndicatorElement.hide();
assertEquals(alertIndicatorStyle.opacity, '0');
assertEquals(alertIndicatorStyle.maxWidth, '0px');
await hideAnimation;
assertFalse(alertIndicatorElement.isConnected);
});
test('multiple calls to show only animates once', () => {
alertIndicatorElement.overrideFadeDurationForTesting(1000);
alertIndicatorElement.show();
alertIndicatorElement.overrideFadeDurationForTesting(0);
alertIndicatorElement.show();
assertNotEquals(alertIndicatorStyle.opacity, '1');
assertNotEquals(alertIndicatorStyle.maxWidth, '16px');
});
test('multiple calls to hide only animates once', () => {
alertIndicatorElement.overrideFadeDurationForTesting(1000);
alertIndicatorElement.hide();
alertIndicatorElement.overrideFadeDurationForTesting(0);
alertIndicatorElement.hide();
assertNotEquals(alertIndicatorStyle.opacity, '0');
assertNotEquals(alertIndicatorStyle.maxWidth, '0px');
});
test(
'calls to show the element while animating to hide cancels ' +
'the hide animation',
() => {
alertIndicatorElement.overrideFadeDurationForTesting(1000);
// This hide promise will be rejected, so catch the rejection.
alertIndicatorElement.hide().then(() => {}, () => {});
alertIndicatorElement.overrideFadeDurationForTesting(0);
alertIndicatorElement.show();
assertEquals(alertIndicatorStyle.opacity, '1');
assertEquals(alertIndicatorStyle.maxWidth, '16px');
});
test(
'calls to hide the element while animating to show cancels ' +
'the show animation',
async () => {
alertIndicatorElement.overrideFadeDurationForTesting(1000);
alertIndicatorElement.show();
alertIndicatorElement.overrideFadeDurationForTesting(0);
const hideAnimation = alertIndicatorElement.hide();
assertEquals(alertIndicatorStyle.opacity, '0');
assertEquals(alertIndicatorStyle.maxWidth, '0px');
await hideAnimation;
assertFalse(alertIndicatorElement.isConnected);
});
});
// 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://tab-strip/alert_indicators.js';
import {TabAlertState} from 'chrome://tab-strip/tabs_api_proxy.js';
suite('AlertIndicators', () => {
let alertIndicatorsElement;
const getAlertIndicators = () => {
return alertIndicatorsElement.shadowRoot.querySelectorAll(
'tabstrip-alert-indicator');
};
setup(() => {
document.body.innerHTML = '';
alertIndicatorsElement =
document.createElement('tabstrip-alert-indicators');
alertIndicatorsElement.onAlertIndicatorCountChange = () => {};
document.body.appendChild(alertIndicatorsElement);
});
test('creates an alert indicator for each alert state', () => {
alertIndicatorsElement.updateAlertStates([
TabAlertState.PIP_PLAYING,
TabAlertState.VR_PRESENTING_IN_HEADSET,
]);
const createdAlertIndicators = getAlertIndicators();
assertEquals(createdAlertIndicators.length, 2);
});
test(
're-uses a shared alert indicator when necessary and prioritizes ' +
'the earlier alert state in the list',
async () => {
async function assertSharedIndicator(prioritizedState, ignoredState) {
await alertIndicatorsElement.updateAlertStates([ignoredState]);
let alertIndicators = getAlertIndicators();
const sharedIndicator = alertIndicators[0];
assertEquals(alertIndicators.length, 1);
assertEquals(sharedIndicator.alertState, ignoredState);
await alertIndicatorsElement.updateAlertStates(
[prioritizedState, ignoredState]);
alertIndicators = getAlertIndicators();
assertEquals(alertIndicators.length, 1);
assertEquals(alertIndicators[0], sharedIndicator);
assertEquals(sharedIndicator.alertState, prioritizedState);
}
await assertSharedIndicator(
TabAlertState.AUDIO_MUTING, TabAlertState.AUDIO_PLAYING);
await assertSharedIndicator(
TabAlertState.MEDIA_RECORDING, TabAlertState.DESKTOP_CAPTURING);
});
test('removes alert indicators when needed', async () => {
await alertIndicatorsElement.updateAlertStates([
TabAlertState.PIP_PLAYING,
TabAlertState.VR_PRESENTING_IN_HEADSET,
]);
await alertIndicatorsElement.updateAlertStates([TabAlertState.PIP_PLAYING]);
const alertIndicators = getAlertIndicators();
assertEquals(alertIndicators.length, 1);
assertEquals(alertIndicators[0].alertState, TabAlertState.PIP_PLAYING);
});
test(
'updating alert states returns a promise with a returned value ' +
'representing the number of alert indicators',
async () => {
assertEquals(
await alertIndicatorsElement.updateAlertStates([
TabAlertState.PIP_PLAYING,
TabAlertState.VR_PRESENTING_IN_HEADSET,
]),
2);
assertEquals(
await alertIndicatorsElement.updateAlertStates([
TabAlertState.PIP_PLAYING,
]),
1);
assertEquals(
await alertIndicatorsElement.updateAlertStates([
TabAlertState.AUDIO_MUTING,
TabAlertState.AUDIO_PLAYING,
]),
1);
});
test(
'updating alert states multiple times in succession resolves with the ' +
'last update',
async () => {
alertIndicatorsElement.updateAlertStates([
TabAlertState.PIP_PLAYING,
TabAlertState.VR_PRESENTING_IN_HEADSET,
]);
alertIndicatorsElement.updateAlertStates([]);
await alertIndicatorsElement.updateAlertStates([
TabAlertState.PIP_PLAYING,
TabAlertState.AUDIO_PLAYING,
]);
const alertIndicators = getAlertIndicators();
assertEquals(alertIndicators.length, 2);
assertEquals(alertIndicators[0].alertState, TabAlertState.PIP_PLAYING);
assertEquals(
alertIndicators[1].alertState, TabAlertState.AUDIO_PLAYING);
});
});
...@@ -58,18 +58,21 @@ suite('TabList', () => { ...@@ -58,18 +58,21 @@ suite('TabList', () => {
const tabs = [ const tabs = [
{ {
active: true, active: true,
alertStates: [],
id: 0, id: 0,
index: 0, index: 0,
title: 'Tab 1', title: 'Tab 1',
}, },
{ {
active: false, active: false,
alertStates: [],
id: 1, id: 1,
index: 1, index: 1,
title: 'Tab 2', title: 'Tab 2',
}, },
{ {
active: false, active: false,
alertStates: [],
id: 2, id: 2,
index: 2, index: 2,
title: 'Tab 3', title: 'Tab 3',
...@@ -144,6 +147,7 @@ suite('TabList', () => { ...@@ -144,6 +147,7 @@ suite('TabList', () => {
test('adds a new tab element when a tab is added in same window', () => { test('adds a new tab element when a tab is added in same window', () => {
const appendedTab = { const appendedTab = {
alertStates: [],
id: 3, id: 3,
index: 3, index: 3,
title: 'New tab', title: 'New tab',
...@@ -154,6 +158,7 @@ suite('TabList', () => { ...@@ -154,6 +158,7 @@ suite('TabList', () => {
assertEquals(tabElements[tabs.length].tab, appendedTab); assertEquals(tabElements[tabs.length].tab, appendedTab);
const prependedTab = { const prependedTab = {
alertStates: [],
id: 4, id: 4,
index: 0, index: 0,
title: 'New tab', title: 'New tab',
...@@ -166,6 +171,7 @@ suite('TabList', () => { ...@@ -166,6 +171,7 @@ suite('TabList', () => {
test('adds a new tab element to the start when it is active', async () => { test('adds a new tab element to the start when it is active', async () => {
const newActiveTab = { const newActiveTab = {
alertStates: [],
active: true, active: true,
id: 3, id: 3,
index: 3, index: 3,
...@@ -205,6 +211,7 @@ suite('TabList', () => { ...@@ -205,6 +211,7 @@ suite('TabList', () => {
test('adds a pinned tab to its designated container', () => { test('adds a pinned tab to its designated container', () => {
webUIListenerCallback('tab-created', { webUIListenerCallback('tab-created', {
alertStates: [],
index: 0, index: 0,
title: 'New pinned tab', title: 'New pinned tab',
pinned: true, pinned: true,
......
...@@ -32,7 +32,7 @@ var TabStripBrowserTest = class extends testing.Test { ...@@ -32,7 +32,7 @@ var TabStripBrowserTest = class extends testing.Test {
var TabStripTabListTest = class extends TabStripBrowserTest { var TabStripTabListTest = class extends TabStripBrowserTest {
get browsePreload() { get browsePreload() {
return 'chrome://test?module=tab_strip/tab_list_test.js'; return 'chrome://tab-strip/test_loader.html?module=tab_strip/tab_list_test.js';
} }
}; };
...@@ -42,10 +42,30 @@ TEST_F('TabStripTabListTest', 'All', function() { ...@@ -42,10 +42,30 @@ TEST_F('TabStripTabListTest', 'All', function() {
var TabStripTabTest = class extends TabStripBrowserTest { var TabStripTabTest = class extends TabStripBrowserTest {
get browsePreload() { get browsePreload() {
return 'chrome://test?module=tab_strip/tab_test.js'; return 'chrome://tab-strip/test_loader.html?module=tab_strip/tab_test.js';
} }
}; };
TEST_F('TabStripTabTest', 'All', function() { TEST_F('TabStripTabTest', 'All', function() {
mocha.run(); mocha.run();
}); });
var TabStripAlertIndicatorsTest = class extends TabStripBrowserTest {
get browsePreload() {
return 'chrome://tab-strip/test_loader.html?module=tab_strip/alert_indicators_test.js';
}
};
TEST_F('TabStripAlertIndicatorsTest', 'All', function() {
mocha.run();
});
var TabStripAlertIndicatorTest = class extends TabStripBrowserTest {
get browsePreload() {
return 'chrome://tab-strip/test_loader.html?module=tab_strip/alert_indicator_test.js';
}
};
TEST_F('TabStripAlertIndicatorTest', 'All', function() {
mocha.run();
});
...@@ -17,6 +17,7 @@ suite('Tab', function() { ...@@ -17,6 +17,7 @@ suite('Tab', function() {
let tabElement; let tabElement;
const tab = { const tab = {
alertStates: [],
id: 1001, id: 1001,
networkState: TabNetworkState.NONE, networkState: TabNetworkState.NONE,
title: 'My title', title: 'My title',
......
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