Commit bf41a9fe authored by Jesse Schettler's avatar Jesse Schettler Committed by Commit Bot

scanning: Add dropdown for scanners

Add a new Polymer element, scanner-select, to display connected scanners
in a dropdown. Styling and finalized strings will be added in subsequent
CLs.

Bug: 1059779
Change-Id: Ia913879262441094c7f7362b648f990943fde907
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2378138
Commit-Queue: Jesse Schettler <jschettler@chromium.org>
Reviewed-by: default avatarJimmy Gong <jimmyxgong@chromium.org>
Cr-Commit-Position: refs/heads/master@{#805505}
parent 6a3c43ed
...@@ -26,6 +26,8 @@ ScanningAppBrowserTest.prototype = { ...@@ -26,6 +26,8 @@ ScanningAppBrowserTest.prototype = {
extraLibraries: [ extraLibraries: [
'//third_party/mocha/mocha.js', '//third_party/mocha/mocha.js',
'//chrome/test/data/webui/mocha_adapter.js', '//chrome/test/data/webui/mocha_adapter.js',
'//ui/webui/resources/js/assert.js',
'//ui/webui/resources/js/promise_resolver.js',
], ],
featureList: { featureList: {
......
...@@ -6,10 +6,117 @@ ...@@ -6,10 +6,117 @@
import 'chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js'; import 'chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js';
import 'chrome://scanning/scanning_app.js'; import 'chrome://scanning/scanning_app.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {setScanServiceForTesting} from 'chrome://scanning/mojo_interface_provider.js';
import {ScannerArr} from 'chrome://scanning/scanning_app_types.js';
import {tokenToString} from 'chrome://scanning/scanning_app_util.js';
/**
* @param {!mojoBase.mojom.UnguessableToken} id
* @param {!string} displayName
* @return {!chromeos.scanning.mojom.Scanner}
*/
function createScanner(id, displayName) {
let scanner = {
'id': id,
'displayName': strToMojoString16(displayName),
};
return scanner;
}
/**
* Converts a JS string to a mojo_base::mojom::String16 object.
* @param {!string} str
* @return {!object}
*/
function strToMojoString16(str) {
let arr = [];
for (var i = 0; i < str.length; i++) {
arr[i] = str.charCodeAt(i);
}
return {data: arr};
}
class FakeScanService {
constructor() {
/** @private {!Map<string, !PromiseResolver>} */
this.resolverMap_ = new Map();
/** @private {!ScannerArr} */
this.scanners_ = [];
this.resetForTest();
}
resetForTest() {
this.scanners_ = [];
this.resolverMap_.set('getScanners', new PromiseResolver());
}
/**
* @param {string} methodName
* @return {!PromiseResolver}
* @private
*/
getResolver_(methodName) {
let method = this.resolverMap_.get(methodName);
assert(!!method, `Method '${methodName}' not found.`);
return method;
}
/**
* @param {string} methodName
* @protected
*/
methodCalled(methodName) {
this.getResolver_(methodName).resolve();
}
/**
* @param {string} methodName
* @return {!Promise}
*/
whenCalled(methodName) {
return this.getResolver_(methodName).promise.then(() => {
// Support sequential calls to whenCalled() by replacing the promise.
this.resolverMap_.set(methodName, new PromiseResolver());
});
}
/** @param {!ScannerArr} scanners */
setScanners(scanners) {
this.scanners_ = scanners;
}
/** @param {chromeos.scanning.mojom.Scanner} scanner */
addScanner(scanner) {
this.scanners_ = this.scanners_.concat(scanner);
}
// scanService methods:
/** @return {!Promise<{scanners: !ScannerArr}>} */
getScanners() {
return new Promise(resolve => {
this.methodCalled('getScanners');
resolve({scanners: this.scanners_ || []});
});
}
}
suite('ScanningAppTest', () => { suite('ScanningAppTest', () => {
/** @type {?ScanningAppElement} */ /** @type {?ScanningAppElement} */
let page = null; let page = null;
/** @type {?chromeos.scanning.mojom.ScanServiceRemote} */
let fakeScanService_;
suiteSetup(() => {
fakeScanService_ = new FakeScanService();
setScanServiceForTesting(fakeScanService_);
});
setup(function() { setup(function() {
PolymerTest.clearBody(); PolymerTest.clearBody();
page = document.createElement('scanning-app'); page = document.createElement('scanning-app');
...@@ -21,9 +128,92 @@ suite('ScanningAppTest', () => { ...@@ -21,9 +128,92 @@ suite('ScanningAppTest', () => {
page = null; page = null;
}); });
test('MainPageLoaded', () => { test('MainPageLoaded', () => {});
// TODO(jschettler): Remove this stub test once the page has more });
// capabilities to test.
assertEquals('Chrome OS Scanning', page.$$('#header').textContent); suite('ScannerSelectTest', () => {
/** @type {!ScannerSelectElement} */
let scannerSelect;
setup(() => {
scannerSelect = document.createElement('scanner-select');
assertTrue(!!scannerSelect);
scannerSelect.loaded = false;
document.body.appendChild(scannerSelect);
});
teardown(() => {
scannerSelect.remove();
scannerSelect = null;
});
test('initializeScannerSelect', () => {
// Before options are added, the dropdown should be hidden and the throbber
// should be visible.
const select = scannerSelect.$$('select');
assertTrue(!!select);
assertTrue(select.hidden);
const throbber = scannerSelect.$$('.throbber-container');
assertTrue(!!throbber);
assertFalse(throbber.hidden);
const firstScannerId = {high: 0, low: 1};
const firstScannerName = 'Scanner 1';
const secondScannerId = {high: 0, low: 2};
const secondScannerName = 'Scanner 2';
const scannerArr = [
createScanner(firstScannerId, firstScannerName),
createScanner(secondScannerId, secondScannerName)
];
scannerSelect.scanners = scannerArr;
scannerSelect.loaded = true;
flush();
// Verify that adding scanners and setting loaded to true results in the
// dropdown becoming visible with the correct options.
assertFalse(select.disabled);
assertFalse(select.hidden);
assertTrue(throbber.hidden);
assertEquals(2, select.length);
assertEquals(firstScannerName, select.options[0].textContent.trim());
assertEquals(secondScannerName, select.options[1].textContent.trim());
assertEquals(tokenToString(firstScannerId), select.value);
});
test('scannerSelectDisabled', () => {
const select = scannerSelect.$$('select');
assertTrue(!!select);
let scannerArr = [createScanner({high: 0, low: 1}, 'Scanner 1')];
scannerSelect.scanners = scannerArr;
scannerSelect.loaded = true;
flush();
// Verify the dropdown is disabled when there's only one option.
assertEquals(1, select.length);
assertTrue(select.disabled);
scannerArr =
scannerArr.concat([createScanner({high: 0, low: 2}, 'Scanner 2')]);
scannerSelect.scanners = scannerArr;
flush();
// Verify the dropdown is enabled when there's more than one option.
assertEquals(2, select.length);
assertFalse(select.disabled);
});
test('noScanners', () => {
const select = scannerSelect.$$('select');
assertTrue(!!select);
scannerSelect.loaded = true;
flush();
// Verify the dropdown is disabled and displays the default option when no
// scanners are available.
assertEquals(1, select.length);
assertEquals('No available scanners', select.options[0].textContent.trim());
assertTrue(select.disabled);
}); });
}); });
...@@ -11,17 +11,38 @@ js_type_check("closure_compile_module") { ...@@ -11,17 +11,38 @@ js_type_check("closure_compile_module") {
is_polymer3 = true is_polymer3 = true
deps = [ deps = [
":mojo_interface_provider", ":mojo_interface_provider",
":scanner_select",
":scanning_app", ":scanning_app",
":scanning_app_types",
":scanning_app_util",
]
}
js_library("scanner_select") {
deps = [
":scanning_app_types",
":scanning_app_util",
"//chromeos/components/scanning/mojom:mojom_js_library_for_compile",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
] ]
} }
js_library("scanning_app") { js_library("scanning_app") {
deps = [ deps = [
":mojo_interface_provider", ":mojo_interface_provider",
":scanner_select",
":scanning_app_types",
":scanning_app_util",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled", "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
] ]
} }
js_library("scanning_app_types") {
}
js_library("scanning_app_util") {
}
js_library("mojo_interface_provider") { js_library("mojo_interface_provider") {
deps = [ deps = [
"//chromeos/components/scanning/mojom:mojom_js_library_for_compile", "//chromeos/components/scanning/mojom:mojom_js_library_for_compile",
...@@ -30,5 +51,9 @@ js_library("mojo_interface_provider") { ...@@ -30,5 +51,9 @@ js_library("mojo_interface_provider") {
} }
html_to_js("web_components") { html_to_js("web_components") {
js_files = [ "scanning_app.js" ] js_files = [
"scanner_select.js",
"scanning_app.js",
"throbber_css.js",
]
} }
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
<scanning-app></scanning-app> <scanning-app></scanning-app>
<script type="module" src="scanning_app.js"></script> <script type="module" src="scanning_app.js"></script>
<script type="module" src="mojo_interface_provider.js"></script>
<!-- Below mojo script required to run browser tests --> <!-- Below mojo script required to run browser tests -->
<script src="chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js"> <script src="chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js">
</script> </script>
......
<style include="throbber">
#title {
padding-inline-end: 10px;
height: 32px;
}
#controls {
display: inline-block;
height: 32px;
width: 300px;
}
</style>
<!-- TODO(jschettler): Replace title and default option with i18n strings. -->
<span id="title">Scanner</span>
<div id="controls">
<div class="throbber-container" hidden$="[[loaded]]">
<div class="throbber"></div>
</div>
<!-- TODO(jschettler): Verify this meets a11y expecations (e.g. ChromeVox
should announce when a new option is focused). -->
<select class="md-select" on-change="onSelectedScannerChange_"
hidden$="[[!loaded]]" disabled="[[disabled_]]">
<!-- TODO(jschettler): Figure out why hiding/disabling the option doesn't
remove it from the dropdown. -->
<template is="dom-if" if="[[!scanners.length]]" restamp>
<option value="">
No available scanners
</option>
</template>
<template is="dom-repeat" items="[[scanners]]" as="scanner">
<option value="[[getTokenAsString_(scanner)]]">
[[getScannerDisplayName_(scanner)]]
</option>
</template>
</select>
</div>
// 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 'chrome://resources/mojo/mojo/public/mojom/base/big_buffer.mojom-lite.js';
import 'chrome://resources/mojo/mojo/public/mojom/base/string16.mojom-lite.js';
import 'chrome://resources/mojo/mojo/public/mojom/base/unguessable_token.mojom-lite.js';
import './scanning.mojom-lite.js';
import './throbber_css.js';
import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {ScannerArr} from './scanning_app_types.js';
import {tokenToString} from './scanning_app_util.js';
/** @type {number} */
const NUM_REQUIRED_SCANNERS = 2;
/**
* @fileoverview
* 'scanner-select' displays the connected scanners in a dropdown.
*/
Polymer({
is: 'scanner-select',
_template: html`{__html_template__}`,
properties: {
/** @type {!ScannerArr} */
scanners: {
type: Array,
value: () => [],
},
loaded: Boolean,
/** @private */
disabled_: Boolean,
},
observers: [
'updateDisabled_(scanners.length)',
],
/**
* @param {!chromeos.scanning.mojom.Scanner} scanner
* @return {string}
* @private
*/
getScannerDisplayName_(scanner) {
return scanner.displayName.data.map(ch => String.fromCodePoint(ch))
.join('');
},
/**
* Converts an unguessable token to a string so it can be used as the value of
* an option.
* @param {!chromeos.scanning.mojom.Scanner} scanner
* @return {string}
* @private
*/
getTokenAsString_(scanner) {
return tokenToString(scanner.id);
},
/**
* @param {!Event} event
* @private
*/
onSelectedScannerChange_(event) {
this.fire('selected-scanner-change', event.target);
},
/**
* Disables the dropdown based on the number of available scanners.
* @param {number} numScanners
* @private
*/
updateDisabled_(numScanners) {
this.disabled_ = numScanners < NUM_REQUIRED_SCANNERS;
},
});
<div id="header"></div> <div id="header"></div>
<scanner-select scanners="[[scanners_]]" loaded="[[loaded_]]"></scanner-select>
...@@ -2,8 +2,15 @@ ...@@ -2,8 +2,15 @@
// 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 'chrome://resources/mojo/mojo/public/mojom/base/big_buffer.mojom-lite.js';
import 'chrome://resources/mojo/mojo/public/mojom/base/string16.mojom-lite.js';
import 'chrome://resources/mojo/mojo/public/mojom/base/unguessable_token.mojom-lite.js';
import './scanner_select.js';
import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getScanService} from './mojo_interface_provider.js'; import {getScanService} from './mojo_interface_provider.js';
import {ScannerArr} from './scanning_app_types.js';
import {tokenToString} from './scanning_app_util.js';
/** /**
* @fileoverview * @fileoverview
...@@ -17,6 +24,36 @@ Polymer({ ...@@ -17,6 +24,36 @@ Polymer({
/** @private {?chromeos.scanning.mojom.ScanServiceInterface} */ /** @private {?chromeos.scanning.mojom.ScanServiceInterface} */
scanService_: null, scanService_: null,
/** @private {!Map<!string, !mojoBase.mojom.UnguessableToken>} */
scannerIds_: new Map(),
properties: {
/**
* @type {!ScannerArr}
* @private
*/
scanners_: {
type: Array,
value: () => [],
},
/**
* @type {?mojoBase.mojom.UnguessableToken}
* @private
*/
selectedScannerId_: Object,
/** @private */
loaded_: {
type: Boolean,
value: false,
},
},
listeners: {
'selected-scanner-change': 'onSelectedScannerChange_',
},
/** @override */ /** @override */
created() { created() {
this.scanService_ = getScanService(); this.scanService_ = getScanService();
...@@ -24,7 +61,42 @@ Polymer({ ...@@ -24,7 +61,42 @@ Polymer({
/** @override */ /** @override */
ready() { ready() {
// TODO(jschettler): Remove this once the app has more capabilities. this.scanService_.getScanners().then(this.onScannersReceived_.bind(this));
this.$$('#header').textContent = 'Chrome OS Scanning'; },
/**
* @param {!{scanners: !ScannerArr}} response
* @private
*/
onScannersReceived_(response) {
this.loaded_ = true;
this.scanners_ = response.scanners;
for (const scanner of this.scanners_) {
this.scannerIds_.set(tokenToString(scanner.id), scanner.id);
}
if (!this.scanners_.length) {
return;
}
// Since the first scanner is the default option in the dropdown, set the
// selected ID to the fist scanner's ID until a different scanner is
// selected.
this.selectedScannerId_ = this.scanners_[0].id;
// TODO(jschettler): Get capabilities for the scanner.
},
/**
* @param {!Event} event
* @private
*/
onSelectedScannerChange_(event) {
const value = event.detail.value;
if (!this.scannerIds_.has(value)) {
return;
}
this.selectedScannerId_ = this.scannerIds_.get(value);
// TODO(jschettler): Get capabilities for the selected scanner.
}, },
}); });
...@@ -16,11 +16,16 @@ ...@@ -16,11 +16,16 @@
<include name="IDR_SCANNING_APP_INDEX_HTML" file="index.html" type="BINDATA" compress="gzip" /> <include name="IDR_SCANNING_APP_INDEX_HTML" file="index.html" type="BINDATA" compress="gzip" />
<include name="IDR_SCANNING_MOJO_LITE_JS" file="${root_gen_dir}/chromeos/components/scanning/mojom/scanning.mojom-lite.js" compress="gzip" use_base_dir="false" type="BINDATA" /> <include name="IDR_SCANNING_MOJO_LITE_JS" file="${root_gen_dir}/chromeos/components/scanning/mojom/scanning.mojom-lite.js" compress="gzip" use_base_dir="false" type="BINDATA" />
<include name="IDR_SCANNING_APP_JS" file="${root_gen_dir}/chromeos/components/scanning/resources/scanning_app.js" use_base_dir="false" compress="gzip" type="BINDATA"/> <include name="IDR_SCANNING_APP_JS" file="${root_gen_dir}/chromeos/components/scanning/resources/scanning_app.js" use_base_dir="false" compress="gzip" type="BINDATA"/>
<include name="IDR_SCANNING_APP_SCANNER_SELECT_HTML" file="scanner_select.html" compress="gzip" type="BINDATA"/>
<include name="IDR_SCANNING_APP_SCANNER_SELECT_JS" file="${root_gen_dir}/chromeos/components/scanning/resources/scanner_select.js" use_base_dir="false" compress="gzip" type="BINDATA"/>
<include name="IDR_SCANNING_APP_THROBBER_CSS_JS" file="${root_gen_dir}/chromeos/components/scanning/resources/throbber_css.js" use_base_dir="false" type="BINDATA"/>
<include name="IDR_SCANNING_APP_ICON" file="app_icon_192.png" type="BINDATA" /> <include name="IDR_SCANNING_APP_ICON" file="app_icon_192.png" type="BINDATA" />
</includes> </includes>
<structures> <structures>
<structure name="IDR_SCANNING_APP_MOJO_INTERFACE_PROVIDER_JS" file="mojo_interface_provider.js" type="chrome_html" /> <structure name="IDR_SCANNING_APP_MOJO_INTERFACE_PROVIDER_JS" file="mojo_interface_provider.js" type="chrome_html" />
<structure name="IDR_SCANNING_APP_TYPES_JS" file="scanning_app_types.js" type="chrome_html" />
<structure name="IDR_SCANNING_APP_UTIL_JS" file="scanning_app_util.js" type="chrome_html" />
</structures> </structures>
</release> </release>
</grit> </grit>
// 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.
/**
* @typedef {!Array<!chromeos.scanning.mojom.Scanner>}
*/
export let ScannerArr;
// 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.
/**
* Converts an unguessable token to a string by combining the high and low
* values as strings with a hashtag as the separator.
* @param {!mojoBase.mojom.UnguessableToken} token
* @return {!string}
*/
export function tokenToString(token) {
return `${token.high.toString()}#${token.low.toString()}`;
}
<template>
<style>
.throbber {
background: url(chrome://resources/images/throbber_small.svg) no-repeat;
display: inline-block;
height: 16px;
width: 16px;
}
</style>
</template>
// 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 {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
const template = document.createElement('template');
template.innerHTML = `
<dom-module id="throbber">{__html_template__}</dom-module>
`;
document.body.appendChild(template.content.cloneNode(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