Add Protocol Handlers section to Application Panel

Adding a section for Protocol Handlers in the App Manifest pane so users can view and test their protocol handlers defined in manifest directly from the DevTools without having to go to manifest.json or having to use a separate application/interface/website to test.

Explainer: https://docs.google.com/document/d/1XsKG_yqHEwWYvI2O0drMliDhF3GaLLGTkC2XmETMSrI/edit?usp=sharing

Backend CL for Telemetry: https://chromium-review.googlesource.com/c/chromium/src/+/3664012

Screenshots:
Protocols detected view: https://drive.google.com/file/d/1kNMHSR-q-m5LxGtDNruVbriuvULtGRXm/view?usp=sharing

Protocols not detected view: https://drive.google.com/file/d/1jYtoHpYQXizNFjEKY31yh-OH7R1kK-Bu/view?usp=sharing

Bug: none
Change-Id: I54a4693e5e9ce10e11aa74c0cb1bffa25ee38780
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3640142
Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
Reviewed-by: Chait Pinnamaneni <cpinnamaneni@microsoft.com>
Commit-Queue: Kristin Lee <v-kristinlee@microsoft.com>
Reviewed-by: Michael Liao <michael.liao@microsoft.com>
This commit is contained in:
Kristin Lee
2022-06-09 20:36:13 +00:00
committed by Devtools-frontend LUCI CQ
parent e29b186fe5
commit cc2291cce5
12 changed files with 369 additions and 2 deletions
+2
View File
@@ -847,6 +847,7 @@ grd_files_debug_sources = [
"front_end/panels/application/components/InterestGroupAccessGrid.js",
"front_end/panels/application/components/OriginTrialTreeView.js",
"front_end/panels/application/components/PermissionsPolicySection.js",
"front_end/panels/application/components/ProtocolHandlersView.js",
"front_end/panels/application/components/ReportsGrid.js",
"front_end/panels/application/components/StackTrace.js",
"front_end/panels/application/components/TrustTokensView.js",
@@ -857,6 +858,7 @@ grd_files_debug_sources = [
"front_end/panels/application/components/originTrialTokenRows.css.js",
"front_end/panels/application/components/originTrialTreeView.css.js",
"front_end/panels/application/components/permissionsPolicySection.css.js",
"front_end/panels/application/components/protocolHandlersView.css.js",
"front_end/panels/application/components/reportingApiGrid.css.js",
"front_end/panels/application/components/stackTraceLinkButton.css.js",
"front_end/panels/application/components/stackTraceRow.css.js",
+2 -1
View File
@@ -345,7 +345,8 @@ export enum Action {
PerfPanelTraceImported = 54,
PerfPanelTraceExported = 55,
StackFrameRestarted = 56,
MaxValue = 57,
CaptureTestProtocolClicked = 57,
MaxValue = 58,
}
/* eslint-disable @typescript-eslint/naming-convention */
+21
View File
@@ -2426,6 +2426,9 @@
"panels/application/AppManifestView.ts | primaryManifestIconFromS": {
"message": "Primary manifest icon from {PH1}"
},
"panels/application/AppManifestView.ts | protocolHandlers": {
"message": "Protocol Handlers"
},
"panels/application/AppManifestView.ts | screenshot": {
"message": "Screenshot"
},
@@ -3197,6 +3200,24 @@
"panels/application/components/PermissionsPolicySection.ts | showDetails": {
"message": "Show details"
},
"panels/application/components/ProtocolHandlersView.ts | manifest": {
"message": "manifest"
},
"panels/application/components/ProtocolHandlersView.ts | needHelpReadOur": {
"message": "Need help? Read {PH1}."
},
"panels/application/components/ProtocolHandlersView.ts | protocolDetected": {
"message": "Found valid protocol handler registration in the {PH1}. With the app installed, test the registered protocols."
},
"panels/application/components/ProtocolHandlersView.ts | protocolHandlerRegistrations": {
"message": "URL protocol handler registration for PWAs"
},
"panels/application/components/ProtocolHandlersView.ts | protocolNotDetected": {
"message": "Define protocol handlers in the {PH1} to register your app as a handler for custom protocols when your app is installed."
},
"panels/application/components/ProtocolHandlersView.ts | testProtocol": {
"message": "Test protocol"
},
"panels/application/components/ReportsGrid.ts | destination": {
"message": "Destination"
},
+21
View File
@@ -2426,6 +2426,9 @@
"panels/application/AppManifestView.ts | primaryManifestIconFromS": {
"message": "P̂ŕîḿâŕŷ ḿâńîf́êśt̂ íĉón̂ f́r̂óm̂ {PH1}"
},
"panels/application/AppManifestView.ts | protocolHandlers": {
"message": "P̂ŕôt́ôćôĺ Ĥán̂d́l̂ér̂ś"
},
"panels/application/AppManifestView.ts | screenshot": {
"message": "Ŝćr̂éêńŝh́ôt́"
},
@@ -3197,6 +3200,24 @@
"panels/application/components/PermissionsPolicySection.ts | showDetails": {
"message": "Ŝh́ôẃ d̂ét̂áîĺŝ"
},
"panels/application/components/ProtocolHandlersView.ts | manifest": {
"message": "m̂án̂íf̂éŝt́"
},
"panels/application/components/ProtocolHandlersView.ts | needHelpReadOur": {
"message": "N̂éêd́ ĥél̂ṕ? R̂éâd́ {PH1}."
},
"panels/application/components/ProtocolHandlersView.ts | protocolDetected": {
"message": "F̂óûńd̂ v́âĺîd́ p̂ŕôt́ôćôĺ ĥán̂d́l̂ér̂ ŕêǵîśt̂ŕât́îón̂ ín̂ t́ĥé {PH1}. Ŵít̂h́ t̂h́ê áp̂ṕ îńŝt́âĺl̂éd̂, t́êśt̂ t́ĥé r̂éĝíŝt́êŕêd́ p̂ŕôt́ôćôĺŝ."
},
"panels/application/components/ProtocolHandlersView.ts | protocolHandlerRegistrations": {
"message": "ÛŔL̂ ṕr̂ót̂óĉól̂ h́âńd̂ĺêŕ r̂éĝíŝt́r̂át̂íôń f̂ór̂ ṔŴÁŝ"
},
"panels/application/components/ProtocolHandlersView.ts | protocolNotDetected": {
"message": "D̂éf̂ín̂é p̂ŕôt́ôćôĺ ĥán̂d́l̂ér̂ś îń t̂h́ê {PH1} t́ô ŕêǵîśt̂ér̂ ýôúr̂ áp̂ṕ âś â h́âńd̂ĺêŕ f̂ór̂ ćûśt̂óm̂ ṕr̂ót̂óĉól̂ś ŵh́êń ŷóûŕ âṕp̂ íŝ ín̂śt̂ál̂ĺêd́."
},
"panels/application/components/ProtocolHandlersView.ts | testProtocol": {
"message": "T̂éŝt́ p̂ŕôt́ôćôĺ"
},
"panels/application/components/ReportsGrid.ts | destination": {
"message": "D̂éŝt́îńât́îón̂"
},
@@ -15,6 +15,7 @@ import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import type * as Protocol from '../../generated/protocol.js';
import * as IconButton from '../../ui/components/icon_button/icon_button.js';
import * as ApplicationComponents from './components/components.js';
const UIStrings = {
/**
@@ -44,6 +45,10 @@ const UIStrings = {
/**
*@description Text in App Manifest View of the Application panel
*/
protocolHandlers: 'Protocol Handlers',
/**
*@description Text in App Manifest View of the Application panel
*/
icons: 'Icons',
/**
*@description Text for the name of something
@@ -423,6 +428,7 @@ export class AppManifestView extends UI.Widget.VBox implements SDK.TargetManager
private target?: SDK.Target.Target;
private resourceTreeModel?: SDK.ResourceTreeModel.ResourceTreeModel|null;
private serviceWorkerManager?: SDK.ServiceWorkerManager.ServiceWorkerManager|null;
private protocolHandlersView: ApplicationComponents.ProtocolHandlersView.ProtocolHandlersView;
constructor() {
super(true);
@@ -448,8 +454,10 @@ export class AppManifestView extends UI.Widget.VBox implements SDK.TargetManager
this.errorsSection = this.reportView.appendSection(i18nString(UIStrings.errorsAndWarnings));
this.installabilitySection = this.reportView.appendSection(i18nString(UIStrings.installability));
this.identitySection = this.reportView.appendSection(i18nString(UIStrings.identity));
this.presentationSection = this.reportView.appendSection(i18nString(UIStrings.presentation));
const protocolHandlersSection = this.reportView.appendSection(i18nString(UIStrings.protocolHandlers));
this.protocolHandlersView = new ApplicationComponents.ProtocolHandlersView.ProtocolHandlersView();
protocolHandlersSection.contentElement.append(this.protocolHandlersView);
this.iconsSection = this.reportView.appendSection(i18nString(UIStrings.icons), 'report-section-icons');
this.shortcutSections = [];
this.screenshotsSections = [];
@@ -701,6 +709,9 @@ export class AppManifestView extends UI.Widget.VBox implements SDK.TargetManager
this.newNoteUrlField.appendChild(link);
}
const protocolHandlers = parsedManifest['protocol_handlers'] || [];
this.protocolHandlersView.data = {protocolHandlers, manifestLink: url};
const icons = parsedManifest['icons'] || [];
this.iconsSection.clearContent();
@@ -16,6 +16,7 @@ generate_css("css_files") {
"originTrialTokenRows.css",
"originTrialTreeView.css",
"permissionsPolicySection.css",
"protocolHandlersView.css",
"reportingApiGrid.css",
"stackTraceLinkButton.css",
"stackTraceRow.css",
@@ -33,6 +34,7 @@ devtools_module("components") {
"InterestGroupAccessGrid.ts",
"OriginTrialTreeView.ts",
"PermissionsPolicySection.ts",
"ProtocolHandlersView.ts",
"ReportsGrid.ts",
"StackTrace.ts",
"TrustTokensView.ts",
@@ -54,6 +56,7 @@ devtools_module("components") {
"../../../ui/components/expandable_list:bundle",
"../../../ui/components/icon_button:bundle",
"../../../ui/components/report_view:bundle",
"../../../ui/components/text_prompt:bundle",
"../../../ui/components/tree_outline:bundle",
"../../../ui/legacy:bundle",
"../../../ui/legacy/components/source_frame:bundle",
@@ -0,0 +1,182 @@
// Copyright 2022 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 * as Host from '../../../core/host/host.js';
import * as i18n from '../../../core/i18n/i18n.js';
import * as Platform from '../../../core/platform/platform.js';
import * as Buttons from '../../../ui/components/buttons/buttons.js';
import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
import * as IconButton from '../../../ui/components/icon_button/icon_button.js';
import * as Input from '../../../ui/components/input/input.js';
// inspectorCommonStyles is imported for the chrome-select class that is used for the dropdown
// eslint-disable-next-line rulesdir/es_modules_import
import inspectorCommonStyles from '../../../ui/legacy/inspectorCommon.css.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as LitHtml from '../../../ui/lit-html/lit-html.js';
import protocolHandlersViewStyles from './protocolHandlersView.css.js';
const PROTOCOL_DOCUMENT_URL = 'https://web.dev/url-protocol-handler/';
const UIStrings = {
/**
*@description Status message for when protocol handlers are detected in the manifest
*@example {protocolhandler/manifest.json} PH1
*/
protocolDetected:
'Found valid protocol handler registration in the {PH1}. With the app installed, test the registered protocols.',
/**
*@description Status message for when protocol handlers are not detected in the manifest
*@example {protocolhandler/manifest.json} PH1
*/
protocolNotDetected:
'Define protocol handlers in the {PH1} to register your app as a handler for custom protocols when your app is installed.',
/**
*@description Text wrapping a link pointing to more information on handling protocol handlers
*@example {https://example.com/} PH1
*/
needHelpReadOur: 'Need help? Read {PH1}.',
/**
*@description Link text for more information on URL protocol handler registrations for PWAs
*/
protocolHandlerRegistrations: 'URL protocol handler registration for PWAs',
/**
*@description In text hyperlink to the PWA manifest
*/
manifest: 'manifest',
/**
*@description Text for test protocol button
*/
testProtocol: 'Test protocol',
};
const str_ = i18n.i18n.registerUIStrings('panels/application/components/ProtocolHandlersView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
interface HTMLSelectElementEvent extends Event {
target: HTMLSelectElement;
}
interface HTMLInputElementEvent extends Event {
target: HTMLInputElement;
}
export interface ProtocolHandler {
protocol: string;
url: string;
}
export interface ProtocolHandlersData {
protocolHandlers: ProtocolHandler[];
manifestLink: Platform.DevToolsPath.UrlString;
}
export class ProtocolHandlersView extends HTMLElement {
static readonly litTagName = LitHtml.literal`devtools-protocol-handlers-view`;
readonly #shadow = this.attachShadow({mode: 'open'});
#protocolHandlers: ProtocolHandler[] = [];
#manifestLink: Platform.DevToolsPath.UrlString = Platform.DevToolsPath.EmptyUrlString;
#selectedProtocolState: string = '';
#queryInputState: string = '';
set data(data: ProtocolHandlersData) {
const isNewManifest = this.#manifestLink !== data.manifestLink;
this.#protocolHandlers = data.protocolHandlers;
this.#manifestLink = data.manifestLink;
if (isNewManifest) {
this.#update();
}
}
#update(): void {
this.#queryInputState = '';
this.#selectedProtocolState = this.#protocolHandlers[0]?.protocol ?? '';
this.#render();
}
#renderStatusMessage(): LitHtml.TemplateResult {
const manifestInTextLink = UI.XLink.XLink.create(this.#manifestLink, i18nString(UIStrings.manifest));
const statusString = this.#protocolHandlers.length > 0 ? UIStrings.protocolDetected : UIStrings.protocolNotDetected;
const iconData: IconButton.Icon.IconData = {
iconName: this.#protocolHandlers.length > 0 ? 'ic_checkmark_16x16' : 'ic_info_black_18dp',
color: this.#protocolHandlers.length > 0 ? 'var( --color-ic-file-image)' : 'var(--color-link)',
width: '16px',
};
return LitHtml.html`
<div class="protocol-handlers-row status">
<${IconButton.Icon.Icon.litTagName} class="inline-icon" .data=${iconData as IconButton.Icon.IconData}>
</${IconButton.Icon.Icon.litTagName}>
${i18n.i18n.getFormatLocalizedString(str_, statusString, {
PH1: manifestInTextLink,
})}
</div>
`;
}
#renderProtocolTest(): LitHtml.LitTemplate {
if (this.#protocolHandlers.length === 0) {
return LitHtml.nothing;
}
const protocolOptions = this.#protocolHandlers.filter(p => p.protocol)
.map(p => LitHtml.html`<option value=${p.protocol}>${p.protocol}://</option>`);
return LitHtml.html`
<div class="protocol-handlers-row">
<select class="chrome-select protocol-select" @change=${this.#handleProtocolSelect}>
${protocolOptions}
</select>
<input .value=${this.#queryInputState} class="devtools-text-input" type="text" @change=${
this.#handleQueryInputChange}/>
<${Buttons.Button.Button.litTagName} .variant=${Buttons.Button.Variant.PRIMARY} @click=${
this.#handleTestProtocolClick}>
${i18nString(UIStrings.testProtocol)}
</${Buttons.Button.Button.litTagName}>
</div>
`;
}
#handleProtocolSelect = (evt: HTMLSelectElementEvent): void => {
this.#selectedProtocolState = evt.target.value;
};
#handleQueryInputChange = (evt: HTMLInputElementEvent): void => {
this.#queryInputState = evt.target.value;
this.#render();
};
#handleTestProtocolClick = (): void => {
const protocolURL = `${this.#selectedProtocolState}://${this.#queryInputState}` as Platform.DevToolsPath.UrlString;
Host.InspectorFrontendHost.InspectorFrontendHostInstance.openInNewTab(protocolURL);
Host.userMetrics.actionTaken(Host.UserMetrics.Action.CaptureTestProtocolClicked);
};
connectedCallback(): void {
this.#shadow.adoptedStyleSheets = [
protocolHandlersViewStyles,
inspectorCommonStyles,
Input.textInputStyles,
];
}
#render(): void {
const protocolDocLink =
UI.XLink.XLink.create(PROTOCOL_DOCUMENT_URL, i18nString(UIStrings.protocolHandlerRegistrations));
// clang-format off
LitHtml.render(LitHtml.html`
${this.#renderStatusMessage()}
<div class="protocol-handlers-row">
${i18n.i18n.getFormatLocalizedString(str_, UIStrings.needHelpReadOur, {PH1: protocolDocLink})}
</div>
${this.#renderProtocolTest()}
`, this.#shadow, {host: this});
// clang-format on
}
}
ComponentHelpers.CustomElements.defineComponent('devtools-protocol-handlers-view', ProtocolHandlersView);
declare global {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface HTMLElementTagNameMap {
'devtools-protocol-handlers-view': ProtocolHandlersView;
}
}
@@ -7,6 +7,7 @@ import * as EndpointsGrid from './EndpointsGrid.js';
import * as FrameDetailsView from './FrameDetailsView.js';
import * as InterestGroupAccessGrid from './InterestGroupAccessGrid.js';
import * as OriginTrialTreeView from './OriginTrialTreeView.js';
import * as ProtocolHandlersView from './ProtocolHandlersView.js';
import * as ReportsGrid from './ReportsGrid.js';
import * as StackTrace from './StackTrace.js';
import * as TrustTokensView from './TrustTokensView.js';
@@ -17,6 +18,7 @@ export {
FrameDetailsView,
InterestGroupAccessGrid,
OriginTrialTreeView,
ProtocolHandlersView,
ReportsGrid,
StackTrace,
TrustTokensView,
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2022 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.
*/
.devtools-link {
color: var(--color-link);
text-decoration: underline;
cursor: pointer;
padding: 2px 0; /* adjust focus ring size */
}
input.devtools-text-input[type="text"] {
padding: 3px 6px;
margin-left: 4px;
margin-right: 4px;
width: 250px;
height: 25px;
}
.protocol-handlers-row {
margin: 10px 0 2px 18px;
}
.inline-icon {
vertical-align: text-bottom;
}
@media (forced-colors: active) {
.devtools-link:not(.devtools-link-prevent-click) {
color: linktext;
}
.devtools-link:focus-visible {
background: Highlight;
color: HighlightText;
}
}
+7
View File
@@ -18,6 +18,13 @@ export async function navigateToServiceWorkers() {
await click(SERVICE_WORKER_ROW_SELECTOR);
}
export async function navigateToManifestInApplicationTab(testName: string) {
const MANIFEST_SELECTOR = '[aria-label="Manifest"]';
const {target} = getBrowserAndPages();
await navigateToApplicationTab(target, testName);
await click(MANIFEST_SELECTOR);
}
export async function doubleClickSourceTreeItem(selector: string) {
const element = await waitFor(selector);
element.evaluate(el => el.scrollIntoView(true));
@@ -11,6 +11,7 @@ ts_library("components") {
"FrameDetailsView_test.ts",
"InterestGroupAccessGrid_test.ts",
"OriginTrialTreeView_test.ts",
"ProtocolHandlersView_test.ts",
"ReportsGrid_test.ts",
"StackTrace_test.ts",
"TrustTokensView_test.ts",
@@ -0,0 +1,77 @@
// Copyright 2022 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 * as ApplicationComponents from '../../../../../../front_end/panels/application/components/components.js';
import type * as Platform from '../../../../../../front_end/core/platform/platform.js';
import {
assertElement,
assertShadowRoot,
getCleanTextContentFromElements,
getElementWithinComponent,
renderElementIntoDOM,
} from '../../../helpers/DOMHelpers.js';
import {describeWithEnvironment} from '../../../helpers/EnvironmentHelpers.js';
const {assert} = chai;
async function renderProtocolHandlersComponent(
manifestLink: Platform.DevToolsPath.UrlString,
protocolHandlers: ApplicationComponents.ProtocolHandlersView.ProtocolHandler[]) {
const component = new ApplicationComponents.ProtocolHandlersView.ProtocolHandlersView();
renderElementIntoDOM(component);
component.data = {manifestLink, protocolHandlers};
return component;
}
describeWithEnvironment('ProtocolHandlersView', () => {
it('renders view when protocols are detected', async () => {
const protocols = [
{
'protocol': 'web+coffee',
'url': './?coffee=%s',
},
{
'protocol': 'web+pwinter',
'url': 'index.html?colors=%s',
},
{
'invalid-protocol': 'this is an invalid protocol entry for testing purposes',
},
];
const manifestURL = 'https://www.example.com/index.html/manifest-protocol.json' as Platform.DevToolsPath.UrlString;
const component = await renderProtocolHandlersComponent(
manifestURL, protocols as ApplicationComponents.ProtocolHandlersView.ProtocolHandler[]);
assertShadowRoot(component.shadowRoot);
const statusElement = component.shadowRoot.querySelector('.protocol-handlers-row.status');
assertElement(statusElement, HTMLElement);
// Tests if status message for when protocols are detected in the manifest is rendering
const protocolsDetectedMessage = getCleanTextContentFromElements(statusElement, 'span');
const expectedStatusMessage =
'Found valid protocol handler registration in the manifest. With the app installed, test the registered protocols.';
assert.deepEqual(protocolsDetectedMessage[0], expectedStatusMessage);
// Tests if protocols are rendering properly in dropdown
const selectElement = getElementWithinComponent(component, '.protocol-select', HTMLSelectElement);
const values = getCleanTextContentFromElements(selectElement, 'option');
assert.deepEqual(values, ['web+coffee://', 'web+pwinter://']);
});
it('renders protocols not detected status message', async () => {
const protocols: ApplicationComponents.ProtocolHandlersView.ProtocolHandler[] = [];
const manifestURL = 'https://www.example.com/index.html/manifest-protocol.json' as Platform.DevToolsPath.UrlString;
const component = await renderProtocolHandlersComponent(
manifestURL, protocols as ApplicationComponents.ProtocolHandlersView.ProtocolHandler[]);
assertShadowRoot(component.shadowRoot);
const noStatusElement = component.shadowRoot.querySelector('.protocol-handlers-row.status');
assertElement(noStatusElement, HTMLElement);
const protocolsNotDetectedMessage = getCleanTextContentFromElements(noStatusElement, 'span');
const expectedStatusMessage =
'Define protocol handlers in the manifest to register your app as a handler for custom protocols when your app is installed.';
assert.deepEqual(protocolsNotDetectedMessage[0], expectedStatusMessage);
});
});