DevTools: Revamp Audits2 UI
- Restructures Audits2 file organization
- Revamps design to match latest mocks
Change-Id: I302ff348ae939b4e48e7b79883fe77ffb97825cf
Reviewed-on: https://chromium-review.googlesource.com/1058708
Commit-Queue: Patrick Hulce <phulce@chromium.org>
Reviewed-by: Paul Irish <paulirish@chromium.org>
Reviewed-by: Dmitry Gozman <dgozman@chromium.org>
Cr-Original-Commit-Position: refs/heads/master@{#559754}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 3f2dc028ad0011e5d68b17bf41870a9296181f28
diff --git a/front_end/audits2/Audits2Controller.js b/front_end/audits2/Audits2Controller.js
new file mode 100644
index 0000000..6a58490
--- /dev/null
+++ b/front_end/audits2/Audits2Controller.js
@@ -0,0 +1,285 @@
+// Copyright 2018 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.
+
+/**
+ * @implements {SDK.SDKModelObserver<!SDK.ServiceWorkerManager>}
+ * @unrestricted
+ */
+Audits2.AuditController = class extends Common.Object {
+ constructor(protocolService) {
+ super();
+
+ protocolService.registerStatusCallback(
+ message => this.dispatchEventToListeners(Audits2.Events.AuditProgressChanged, {message}));
+
+ for (const preset of Audits2.Presets)
+ preset.setting.addChangeListener(this.recomputePageAuditability.bind(this));
+
+ SDK.targetManager.observeModels(SDK.ServiceWorkerManager, this);
+ SDK.targetManager.addEventListener(
+ SDK.TargetManager.Events.InspectedURLChanged, this.recomputePageAuditability, this);
+ }
+
+ /**
+ * @override
+ * @param {!SDK.ServiceWorkerManager} serviceWorkerManager
+ */
+ modelAdded(serviceWorkerManager) {
+ if (this._manager)
+ return;
+
+ this._manager = serviceWorkerManager;
+ this._serviceWorkerListeners = [
+ this._manager.addEventListener(
+ SDK.ServiceWorkerManager.Events.RegistrationUpdated, this.recomputePageAuditability, this),
+ this._manager.addEventListener(
+ SDK.ServiceWorkerManager.Events.RegistrationDeleted, this.recomputePageAuditability, this),
+ ];
+
+ this.recomputePageAuditability();
+ }
+
+ /**
+ * @override
+ * @param {!SDK.ServiceWorkerManager} serviceWorkerManager
+ */
+ modelRemoved(serviceWorkerManager) {
+ if (this._manager !== serviceWorkerManager)
+ return;
+
+ Common.EventTarget.removeEventListeners(this._serviceWorkerListeners);
+ this._manager = null;
+ this.recomputePageAuditability();
+ }
+
+ /**
+ * @return {boolean}
+ */
+ _hasActiveServiceWorker() {
+ if (!this._manager)
+ return false;
+
+ const mainTarget = this._manager.target();
+ if (!mainTarget)
+ return false;
+
+ const inspectedURL = mainTarget.inspectedURL().asParsedURL();
+ const inspectedOrigin = inspectedURL && inspectedURL.securityOrigin();
+ for (const registration of this._manager.registrations().values()) {
+ if (registration.securityOrigin !== inspectedOrigin)
+ continue;
+
+ for (const version of registration.versions.values()) {
+ if (version.controlledClients.length > 1)
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @return {boolean}
+ */
+ _hasAtLeastOneCategory() {
+ return Audits2.Presets.some(preset => preset.setting.get());
+ }
+
+ /**
+ * @return {?string}
+ */
+ _unauditablePageMessage() {
+ if (!this._manager)
+ return null;
+
+ const mainTarget = this._manager.target();
+ const inspectedURL = mainTarget && mainTarget.inspectedURL();
+ if (inspectedURL && !/^(http|chrome-extension)/.test(inspectedURL)) {
+ return Common.UIString(
+ 'Can only audit HTTP/HTTPS pages and Chrome extensions. ' +
+ 'Navigate to a different page to start an audit.');
+ }
+
+ // Audits don't work on most undockable targets (extension popup pages, remote debugging, etc).
+ // However, the tests run in a content shell which is not dockable yet audits just fine,
+ // so disable this check when under test.
+ if (!Host.isUnderTest() && !Runtime.queryParam('can_dock'))
+ return Common.UIString('Can only audit tabs. Navigate to this page in a separate tab to start an audit.');
+
+ return null;
+ }
+
+ /**
+ * @return {!Promise<string>}
+ */
+ async _evaluateInspectedURL() {
+ const mainTarget = this._manager.target();
+ const runtimeModel = mainTarget.model(SDK.RuntimeModel);
+ const executionContext = runtimeModel && runtimeModel.defaultExecutionContext();
+ let inspectedURL = mainTarget.inspectedURL();
+ if (!executionContext)
+ return inspectedURL;
+
+ // Evaluate location.href for a more specific URL than inspectedURL provides so that SPA hash navigation routes
+ // will be respected and audited.
+ try {
+ const result = await executionContext.evaluate(
+ {
+ expression: 'window.location.href',
+ objectGroup: 'audits',
+ includeCommandLineAPI: false,
+ silent: false,
+ returnByValue: true,
+ generatePreview: false
+ },
+ /* userGesture */ false, /* awaitPromise */ false);
+ if (!result.exceptionDetails && result.object) {
+ inspectedURL = result.object.value;
+ result.object.release();
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ return inspectedURL;
+ }
+
+ /**
+ * @return {!Object}
+ */
+ getFlags() {
+ const flags = {};
+ for (const runtimeSetting of Audits2.RuntimeSettings)
+ runtimeSetting.setFlags(flags, runtimeSetting.setting.get());
+ return flags;
+ }
+
+ /**
+ * @return {!Array<string>}
+ */
+ getCategoryIDs() {
+ const categoryIDs = [];
+ for (const preset of Audits2.Presets) {
+ if (preset.setting.get())
+ categoryIDs.push(preset.configID);
+ }
+ return categoryIDs;
+ }
+
+ /**
+ * @param {{force: boolean}=} options
+ * @return {!Promise<string>}
+ */
+ async getInspectedURL(options) {
+ if (options && options.force || !this._inspectedURL)
+ this._inspectedURL = await this._evaluateInspectedURL();
+ return this._inspectedURL;
+ }
+
+ recomputePageAuditability() {
+ const hasActiveServiceWorker = this._hasActiveServiceWorker();
+ const hasAtLeastOneCategory = this._hasAtLeastOneCategory();
+ const unauditablePageMessage = this._unauditablePageMessage();
+
+ let helpText = '';
+ if (hasActiveServiceWorker) {
+ helpText = Common.UIString(
+ 'Multiple tabs are being controlled by the same service worker. ' +
+ 'Close your other tabs on the same origin to audit this page.');
+ } else if (!hasAtLeastOneCategory) {
+ helpText = Common.UIString('At least one category must be selected.');
+ } else if (unauditablePageMessage) {
+ helpText = unauditablePageMessage;
+ }
+
+ this.dispatchEventToListeners(Audits2.Events.PageAuditabilityChanged, {helpText});
+ }
+};
+
+
+/** @typedef {{type: string, setting: !Common.Setting, configID: string, title: string, description: string}} */
+Audits2.Preset;
+
+/** @type {!Array.<!Audits2.Preset>} */
+Audits2.Presets = [
+ // configID maps to Lighthouse's Object.keys(config.categories)[0] value
+ {
+ setting: Common.settings.createSetting('audits2.cat_perf', true),
+ configID: 'performance',
+ title: 'Performance',
+ description: 'How long does this app take to show content and become usable'
+ },
+ {
+ setting: Common.settings.createSetting('audits2.cat_pwa', true),
+ configID: 'pwa',
+ title: 'Progressive Web App',
+ description: 'Does this page meet the standard of a Progressive Web App'
+ },
+ {
+ setting: Common.settings.createSetting('audits2.cat_best_practices', true),
+ configID: 'best-practices',
+ title: 'Best practices',
+ description: 'Does this page follow best practices for modern web development'
+ },
+ {
+ setting: Common.settings.createSetting('audits2.cat_a11y', true),
+ configID: 'accessibility',
+ title: 'Accessibility',
+ description: 'Is this page usable by people with disabilities or impairments'
+ },
+ {
+ setting: Common.settings.createSetting('audits2.cat_seo', true),
+ configID: 'seo',
+ title: 'SEO',
+ description: 'Is this page optimized for search engine results ranking'
+ },
+];
+
+/** @typedef {{setting: !Common.Setting, description: string, setFlags: function(!Object, string), options: !Array}} */
+Audits2.RuntimeSetting;
+
+/** @type {!Array.<!Audits2.RuntimeSetting>} */
+Audits2.RuntimeSettings = [
+ {
+ setting: Common.settings.createSetting('audits2.device_type', 'mobile'),
+ description: Common.UIString('Apply mobile emulation during auditing'),
+ setFlags: (flags, value) => {
+ flags.disableDeviceEmulation = value === 'desktop';
+ },
+ options: [
+ {label: Common.UIString('Mobile'), value: 'mobile'},
+ {label: Common.UIString('Desktop'), value: 'desktop'},
+ ],
+ },
+ {
+ setting: Common.settings.createSetting('audits2.throttling', 'default'),
+ description: Common.UIString('Apply network and CPU throttling during performance auditing'),
+ setFlags: (flags, value) => {
+ flags.disableNetworkThrottling = value === 'off';
+ flags.disableCpuThrottling = value === 'off';
+ },
+ options: [
+ {label: Common.UIString('3G w/ CPU slowdown'), value: 'default'},
+ {label: Common.UIString('No throttling'), value: 'off'},
+ ],
+ },
+ {
+ setting: Common.settings.createSetting('audits2.storage_reset', 'on'),
+ description: Common.UIString('Reset storage (localStorage, IndexedDB, etc) to a clean baseline before auditing'),
+ setFlags: (flags, value) => {
+ flags.disableStorageReset = value === 'off';
+ },
+ options: [
+ {label: Common.UIString('Clear storage'), value: 'on'},
+ {label: Common.UIString('Preserve storage'), value: 'off'},
+ ],
+ },
+];
+
+Audits2.Events = {
+ PageAuditabilityChanged: Symbol('PageAuditabilityChanged'),
+ AuditProgressChanged: Symbol('AuditProgressChanged'),
+ RequestAuditStart: Symbol('RequestAuditStart'),
+ RequestAuditCancel: Symbol('RequestAuditCancel'),
+};
\ No newline at end of file