Commit ba330566 authored by Jimmy Gong's avatar Jimmy Gong Committed by Commit Bot

Introduce PrintJobEntry to PrintManagementApp

- This adds a new polymer element that contains a print job in the print
  management app.
- Print jobs will now appear upon app's startup.
- CSS styling + sorting will be implemented in separate CLs.
- Implemented i18n for strings. Current strings are not final.

Screenshot of current status: https://screenshot.googleplex.com/PbiY16Y7d8M

Bug: 1053704
Tests: browsertests
Change-Id: I5c96ec72bd3f528917d5b5c9876ec642a2e87fdb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2125246Reviewed-by: default avatarXiyuan Xia <xiyuan@chromium.org>
Reviewed-by: default avatarScott Violet <sky@chromium.org>
Reviewed-by: default avatarKyle Horimoto <khorimoto@chromium.org>
Reviewed-by: default avatarBailey Berro <baileyberro@chromium.org>
Commit-Queue: jimmy gong <jimmyxgong@chromium.org>
Cr-Commit-Position: refs/heads/master@{#756841}
parent 6f985973
......@@ -24,6 +24,8 @@ PrintManagementBrowserTest.prototype = {
extraLibraries: [
'//third_party/mocha/mocha.js',
'//chrome/test/data/webui/mocha_adapter.js',
'//ui/webui/resources/js/assert.js',
'//ui/webui/resources/js/promise_resolver.js',
],
featureList: {
......
......@@ -6,10 +6,143 @@
import 'chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js';
import 'chrome://print-management/print_management.js';
import {setMetadataProviderForTesting} from 'chrome://print-management/mojo_interface_provider.js';
const CompletionStatus = {
FAILED: 0,
CANCELED: 1,
PRINTED: 2,
};
/**
* Converts a JS string to 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};
}
/**
* Converts a JS time (milliseconds since UNIX epoch) to mojom::time
* (microseconds since WINDOWS epoch).
* @param {Date} jsDate
* @return {number}
*/
function convertToMojoTime(jsDate) {
const windowsEpoch = new Date(Date.UTC(1601, 0, 1, 0, 0, 0));
const jsEpoch = new Date(Date.UTC(1970, 0, 1, 0, 0, 0));
return ((jsEpoch - windowsEpoch) * 1000) + (jsDate.getTime() * 1000);
}
/**
* @param{string} id
* @param{string} title
* @param{number} completionStatus
* @param{Date} jsDate
* @param{string} printerName
* @return {!Object}
*/
function createJobEntry(id, title, completionStatus, jsDate, printerName) {
let jobEntry = {};
jobEntry.id = id;
jobEntry.title = strToMojoString16(title);
jobEntry.completionStatus = completionStatus;
jobEntry.creationTime = {internalValue: convertToMojoTime(jsDate)};
jobEntry.printerName = strToMojoString16(printerName);
jobEntry.printerUri = {url: '192.168.1.1'};
jobEntry.numberOfPages = 1;
return jobEntry;
}
class FakePrintingMetadataProvider {
constructor() {
/** @private {!Map<string, !PromiseResolver>} */
this.resolverMap_ = new Map();
/** @type {!Array<chromeos.printing.printingManager.mojom.PrintJobInfo>} */
this.printJobs_ = [];
this.resetForTest();
}
resetForTest() {
this.printJobs_ = [];
this.resolverMap_.set('getPrintJobs', 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 {chromeos.printing.printingManager.mojom.PrintJobInfo} job
*/
addPrintJob(job) {
this.printJobs_ = this.printJobs_.concat(job);
}
// printingMetadataProvider methods
/**
* @return {!Promise<{printJobs:
* !Array<chromeos.printing.printingManager.mojom.PrintJobInfo>}>}
*/
getPrintJobs() {
return new Promise(resolve => {
this.methodCalled('getPrintJobs');
resolve({printJobs: this.printJobs_ || []});
});
}
}
suite('PrintManagementTest', () => {
/** @type {?PrintManagementElement} */
let page = null;
/**
* @type {
* ?chromeos.printing.printingManager.mojom.PrintingMetadataProviderRemote
* }
*/
let mojoApi_;
suiteSetup(() => {
mojoApi_ = new FakePrintingMetadataProvider();
setMetadataProviderForTesting(mojoApi_);
});
setup(function() {
PolymerTest.clearBody();
page = document.createElement('print-management');
......@@ -26,4 +159,79 @@ suite('PrintManagementTest', () => {
// capabilities to test.
assertEquals('Print Management', page.$$('#header').textContent);
});
});
\ No newline at end of file
});
suite('PrintJobEntryTest', () => {
/** @type {?HTMLElement} */
let jobEntryTestElement = null;
/**
* @type {
* ?chromeos.printing.printingManager.mojom.PrintingMetadataProviderRemote
* }
*/
let mojoApi_;
suiteSetup(() => {
mojoApi_ = new FakePrintingMetadataProvider();
setMetadataProviderForTesting(mojoApi_);
});
setup(() => {
jobEntryTestElement = document.createElement('print-job-entry');
assertTrue(!!jobEntryTestElement);
document.body.appendChild(jobEntryTestElement);
});
teardown(() => {
jobEntryTestElement.remove();
jobEntryTestElement = null;
});
/**
* @param {!HTMLElement} element
* @param {number} newStatus
* @param {string} expectedStatus
*/
function updateAndVerifyCompletionStatus(element, newStatus, expectedStatus) {
element.set('jobEntry.completionStatus', newStatus);
assertEquals(
expectedStatus, element.$$('#completionStatus').textContent.trim());
}
test('initializeJobEntry', () => {
const expectedTitle = 'title';
const expectedStatus = CompletionStatus.PRINTED;
const expectedPrinterName = 'printer name';
const expectedCreationTime = new Date();
jobEntryTestElement.jobEntry = createJobEntry(
/*id=*/ '1', expectedTitle, expectedStatus, expectedCreationTime,
expectedPrinterName);
// Assert the title, printer name, creation time, and status are displayed
// correctly.
assertEquals(
expectedTitle, jobEntryTestElement.$$('#jobTitle').textContent.trim());
assertEquals(
expectedPrinterName,
jobEntryTestElement.$$('#printerName').textContent.trim());
assertEquals(
'Printed',
jobEntryTestElement.$$('#completionStatus').textContent.trim());
// Change date and assert it shows the correct date (Jan 31, 2020);
jobEntryTestElement.set('jobEntry.creationTime', {
internalValue: convertToMojoTime(new Date(Date.UTC(2020, 1, 1, 0, 0, 0)))
});
assertEquals(
'Jan 31, 2020',
jobEntryTestElement.$$('#creationTime').textContent.trim());
// Change the completion status and verify it shows the correct status.
updateAndVerifyCompletionStatus(
jobEntryTestElement, CompletionStatus.FAILED, 'Failed');
updateAndVerifyCompletionStatus(
jobEntryTestElement, CompletionStatus.CANCELED, 'Canceled');
});
});
......@@ -301,6 +301,21 @@ Try tapping the mic to ask me anything.
<message name="IDS_REQUEST_PIN_DIALOG_ERROR_ATTEMPTS" desc="The text displayed in the certificate provider PIN request dialog when the previous login attempt was unsuccessful but there are more attempts remaining. Includes the reason for the previous failure.">
<ph name="ERROR_MESSAGE">$1<ex>Invalid PIN.</ex></ph> <ph name="ATTEMPTS_LEFT">$2<ex>3</ex></ph> attempts left
</message>
<!-- Print Management App -->
<!-- TODO(crbug/1067465): Update with finalized copies of the strings -->
<message name="IDS_PRINT_MANAGEMENT_COMPLETION_STATUS_FAILED" desc="Displayed text status if the print job failed." translateable="false">
Failed
</message>
<message name="IDS_PRINT_MANAGEMENT_COMPLETION_STATUS_CANCELED" desc="Displayed text status if the print job was canceled." translateable="false">
Canceled
</message>
<message name="IDS_PRINT_MANAGEMENT_COMPLETION_STATUS_PRINTED" desc="Displayed text status if the print job was printed succesfully." translateable="false">
Printed
</message>
<message name="IDS_PRINT_MANAGEMENT_COMPLETION_STATUS_UNKNOWN_ERROR" desc="Displayed text status if the print job had an unknown error." translateable="false">
Unknown Error
</message>
</messages>
</release>
</grit>
......@@ -18,7 +18,9 @@ static_library("print_management") {
"//chromeos/components/print_management/mojom",
"//chromeos/constants",
"//chromeos/resources:print_management_resources",
"//chromeos/strings/",
"//content/public/browser",
"//ui/base",
"//ui/resources",
"//ui/webui",
]
......
include_rules = [
"-chrome",
"+chromeos/grit/chromeos_print_management_resources.h",
"+chromeos/strings/grit/chromeos_strings.h",
"+content/public/browser",
"+ui/base",
"+ui/resources",
"+ui/webui",
]
\ No newline at end of file
......@@ -8,14 +8,34 @@
#include "chromeos/components/print_management/mojom/printing_manager.mojom.h"
#include "chromeos/components/print_management/url_constants.h"
#include "chromeos/grit/chromeos_print_management_resources.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_data_source.h"
#include "ui/base/webui/web_ui_util.h"
#include "ui/resources/grit/webui_resources.h"
namespace chromeos {
namespace printing {
namespace printing_manager {
namespace {
void AddPrintManagementStrings(content::WebUIDataSource* html_source) {
static constexpr webui::LocalizedString kLocalizedStrings[] = {
{"completionStatusFailed", IDS_PRINT_MANAGEMENT_COMPLETION_STATUS_FAILED},
{"completionStatusCanceled",
IDS_PRINT_MANAGEMENT_COMPLETION_STATUS_CANCELED},
{"completionStatusPrinted",
IDS_PRINT_MANAGEMENT_COMPLETION_STATUS_PRINTED},
{"completionStatusUnknownError",
IDS_PRINT_MANAGEMENT_COMPLETION_STATUS_UNKNOWN_ERROR},
};
for (const auto& str : kLocalizedStrings) {
html_source->AddLocalizedString(str.name, str.id);
}
html_source->UseStringsJs();
}
} // namespace
PrintManagementUI::PrintManagementUI(
content::WebUI* web_ui,
......@@ -37,8 +57,14 @@ PrintManagementUI::PrintManagementUI(
html_source->AddResourcePath("pwa.html", IDR_PRINT_MANAGEMENT_PWA_HTML);
html_source->AddResourcePath("manifest.json", IDR_PRINT_MANAGEMENT_MANIFEST);
html_source->AddResourcePath("app_icon_192.png", IDR_PRINT_MANAGEMENT_ICON);
html_source->AddResourcePath("print_job_entry.html",
IDR_PRINT_MANAGEMENT_PRINT_JOB_ENTRY_HTML);
html_source->AddResourcePath("print_job_entry.js",
IDR_PRINT_MANAGEMENT_PRINT_JOB_ENTRY_JS);
html_source->SetDefaultResource(IDR_PRINT_MANAGEMENT_INDEX_HTML);
AddPrintManagementStrings(html_source.get());
content::WebUIDataSource::Add(web_ui->GetWebContents()->GetBrowserContext(),
html_source.release());
}
......
......@@ -11,13 +11,23 @@ js_type_check("closure_compile_module") {
is_polymer3 = true
deps = [
":mojo_interface_provider",
":print_job_entry",
":print_management",
]
}
js_library("print_job_entry") {
deps = [
"//chromeos/components/print_management/mojom:mojom_js_library_for_compile",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
"//ui/webui/resources/js:load_time_data.m",
]
}
js_library("print_management") {
deps = [
":mojo_interface_provider",
":print_job_entry",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
]
}
......@@ -35,6 +45,12 @@ polymer_modulizer("print_management") {
html_type = "v3-ready"
}
polymer_modulizer("print_job_entry") {
js_file = "print_job_entry.js"
html_file = "print_job_entry.html"
html_type = "v3-ready"
}
polymer_modulizer("scanning_ui") {
js_file = "scanning_ui.js"
html_file = "scanning_ui.html"
......@@ -43,6 +59,7 @@ polymer_modulizer("scanning_ui") {
group("polymer3_elements") {
public_deps = [
":print_job_entry_module",
":print_management_module",
":scanning_ui_module",
]
......
......@@ -11,6 +11,7 @@
<print-management></print-management>
<script type="module" src="print_management.js"></script>
<script type="module" src="mojo_interface_provider.js"></script>
<!-- Below mojo script required to run browser tests -->
<script src="chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js">
</script>
......
<style>
.list-item {
align-items: center;
display: flex;
min-height: var(--settings-row-min-height);
padding: 0;
}
.long-text {
overflow: hidden;
padding-left: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
.short-text {
padding-left: 10px;
width: 90px;
}
#jobTitle {
width: 250px;
}
#printerName {
width: 150px;
}
</style>
<div class="list-item"
aria-labeledby="jobTitle printerName creationTime completionStatus">
<div id="jobTitle" class="long-text" aria-hidden="true">[[jobTitle_]]</div>
<div id="printerName" class="long-text" aria-hidden="true">
[[printerName_]]
</div>
<div id="creationTime" class="short-text" aria-hidden="true">
[[creationTime_]]
</div>
<div id="completionStatus" class="short-text" aria-hidden="true">
[[completionStatus_]]
</div>
</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/time.mojom-lite.js';
import 'chrome://resources/mojo/url/mojom/url.mojom-lite.js';
import './printing_manager.mojom-lite.js';
import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertNotReached} from 'chrome://resources/js/assert.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import './strings.js';
/**
* Converts a mojo time to a JS time.
* @param {!mojoBase.mojom.Time} mojoTime
* @return {!Date}
*/
function convertMojoTimeToJS(mojoTime) {
// The JS Date() is based off of the number of milliseconds since the
// UNIX epoch (1970-01-01 00::00:00 UTC), while |internalValue| of the
// base::Time (represented in mojom.Time) represents the number of
// microseconds since the Windows FILETIME epoch (1601-01-01 00:00:00 UTC).
// This computes the final JS time by computing the epoch delta and the
// conversion from microseconds to milliseconds.
const windowsEpoch = Date.UTC(1601, 0, 1, 0, 0, 0, 0);
const unixEpoch = Date.UTC(1970, 0, 1, 0, 0, 0, 0);
// |epochDeltaInMs| equals to base::Time::kTimeTToMicrosecondsOffset.
const epochDeltaInMs = unixEpoch - windowsEpoch;
const timeInMs = Number(mojoTime.internalValue) / 1000;
return new Date(timeInMs - epochDeltaInMs);
};
/**
* Returns true if |date| is today, false otherwise.
* @param {!Date} date
* @return {boolean}
*/
function isToday(date) {
const today_date = new Date();
return date.getDate() === today_date.getDate() &&
date.getMonth() === today_date.getMonth() &&
date.getFullYear() === today_date.getFullYear();
};
/**
* @fileoverview
* 'print-job-entry' is contains a single print job entry and is used as a list
* item.
*/
Polymer({
is: 'print-job-entry',
_template: html`{__html_template__}`,
properties: {
/** @type {!chromeos.printing.printingManager.mojom.PrintJobInfo} */
jobEntry: Object,
/** @private */
jobTitle_: {
type: String,
computed: 'decodeString16_(jobEntry.title)',
},
/** @private */
printerName_: {
type: String,
computed: 'decodeString16_(jobEntry.printerName)',
},
/** @private */
creationTime_: {
type: String,
computed: 'computeDate_(jobEntry.creationTime)'
},
/** @private */
completionStatus_: {
type: String,
computed: 'convertStatusToString_(jobEntry.completionStatus)',
},
},
/**
* Converts utf16 to a readable string.
* @param {!mojoBase.mojom.String16} arr
* @return {string}
* @private
*/
decodeString16_(arr) {
return arr.data.map(ch => String.fromCodePoint(ch)).join('');
},
/**
* Converts mojo time to JS time. Returns "Today" if |mojoTime| is at the
* current day.
* @param {!mojoBase.mojom.Time} mojoTime
* @return {string}
* @private
*/
computeDate_(mojoTime) {
const jsDate = convertMojoTimeToJS(mojoTime);
// Date() is constructed with the current time in UTC. If the Date() matches
// |jsDate|'s date, display the 12hour time of the current date.
if (isToday(jsDate)) {
return jsDate.toLocaleTimeString(/*locales=*/undefined,
{hour12: true, hour: 'numeric', minute: 'numeric'});
}
// Remove the day of the week from the date.
return jsDate.toLocaleDateString(/*locales=*/undefined,
{month: 'short', day: 'numeric', year: 'numeric'});
},
/**
* Returns the corresponding completion status from |mojoCompletionStatus|.
* @param {number} mojoCompletionStatus
* @return {string}
* @private
*/
convertStatusToString_(mojoCompletionStatus) {
switch (mojoCompletionStatus) {
case chromeos.printing.printingManager.mojom.PrintJobCompletionStatus
.kFailed:
return loadTimeData.getString('completionStatusFailed');
case chromeos.printing.printingManager.mojom.PrintJobCompletionStatus
.kCanceled:
return loadTimeData.getString('completionStatusCanceled');
case chromeos.printing.printingManager.mojom.PrintJobCompletionStatus
.kPrinted:
return loadTimeData.getString('completionStatusPrinted');
default:
assertNotReached();
return loadTimeData.getString('completionStatusUnknownError');
}
},
});
<style>
#entryList {
padding-inline-start: 10%;
padding-inline-end: 10%;
}
</style>
<div id="header"></div>
<iron-list id="entryList" items="[[printJobs_]]" role="grid">
<template>
<div role="row" tabindex$="[[tabIndex]]">
<print-job-entry job-entry="[[item]]"></print-job-entry>
</div>
</template>
</iron-list>
......@@ -2,9 +2,23 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
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/time.mojom-lite.js';
import 'chrome://resources/mojo/url/mojom/url.mojom-lite.js';
import './print_job_entry.js';
import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getMetadataProvider} from './mojo_interface_provider.js';
/**
* @typedef {{
* printJobs: !Array<!chromeos.printing.printingManager.mojom.PrintJobInfo>
* }}
*/
let PrintJobsList;
/**
* @fileoverview
* 'print-management' is used as the main app to display print jobs.
......@@ -21,6 +35,16 @@ Polymer({
*/
mojoInterfaceProvider_: null,
properties: {
/**
* @type {!Array<!chromeos.printing.printingManager.mojom.PrintJobInfo>}
* @private
*/
printJobs_: {
type: Array,
},
},
/** @override */
created() {
this.mojoInterfaceProvider_ = getMetadataProvider();
......@@ -30,5 +54,9 @@ Polymer({
ready() {
// TODO(jimmyxgong): Remove this once the app has more capabilities.
this.$$('#header').textContent = 'Print Management';
this.mojoInterfaceProvider_.getPrintJobs()
.then((responseParams) => {
this.printJobs_ = responseParams.printJobs;
});
},
});
......@@ -15,12 +15,14 @@
<!-- Privileged app host contents. -->
<include name="IDR_PRINT_MANAGEMENT_INDEX_HTML" file="index.html" type="BINDATA" compress="gzip" />
<include name="IDR_PRINT_MANAGEMENT_JS" file="${root_gen_dir}/chromeos/components/print_management/resources/print_management.js" use_base_dir="false" compress="gzip" type="BINDATA"/>
<include name="IDR_PRINT_MANAGEMENT_PRINT_JOB_ENTRY_HTML" file="print_job_entry.html" compress="gzip" type="BINDATA"/>
<include name="IDR_PRINT_MANAGEMENT_PRINT_JOB_ENTRY_JS" file="${root_gen_dir}/chromeos/components/print_management/resources/print_job_entry.js" use_base_dir="false" compress="gzip" type="BINDATA"/>
<include name="IDR_PRINT_MANAGEMENT_MANIFEST" file="manifest.json" type="BINDATA" compress="gzip" />
<include name="IDR_PRINT_MANAGEMENT_ICON" file="app_icon_192.png" type="BINDATA" compress="gzip" />
<include name="IDR_PRINTING_MANAGER_MOJO_LITE_JS" file="${root_gen_dir}/chromeos/components/print_management/mojom/printing_manager.mojom-lite.js" compress="gzip" use_base_dir="false" type="BINDATA" />
<include name="IDR_PRINT_MANAGEMENT_PWA_HTML" file="pwa.html" type="BINDATA" compress="gzip" />
<include name="IDR_PRINT_MANAGEMENT_PWA_HTML" file="pwa.html" type="BINDATA" compress="gzip" />
<include name="IDR_SCANNING_UI_INDEX_HTML" file="scanning_ui_index.html" type="BINDATA" compress="gzip" />
<include name="IDR_SCANNING_UI_JS" file="${root_gen_dir}/chromeos/components/print_management/resources/scanning_ui.js" use_base_dir="false" compress="gzip" type="BINDATA"/>
<include name="IDR_SCANNING_UI_JS" file="${root_gen_dir}/chromeos/components/print_management/resources/scanning_ui.js" use_base_dir="false" compress="gzip" type="BINDATA"/>
</includes>
<structures>
......
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