chrome-devtools-frontend
Version:
Chrome DevTools UI
470 lines (431 loc) • 17.9 kB
text/typescript
// 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.
/* eslint-disable rulesdir/no_underscored_properties */
import * as Common from '../common/common.js';
import * as i18n from '../i18n/i18n.js';
import * as SDK from '../sdk/sdk.js';
import {ProtocolService} from './LighthouseProtocolService.js'; // eslint-disable-line no-unused-vars
export const UIStrings = {
/**
*@description Explanation for user that Ligthhouse can only audit HTTP/HTTPS pages
*/
canOnlyAuditHttphttpsPagesAnd:
'Can only audit HTTP/HTTPS pages and `Chrome` extensions. Navigate to a different page to start an audit.',
/**
*@description Text when stored data in one location may affect Lighthouse run
*@example {IndexedDB} PH1
*/
thereMayBeStoredDataAffectingSingular:
'There may be stored data affecting loading performance in this location: {PH1}. Audit this page in an incognito window to prevent those resources from affecting your scores.',
/**
*@description Text when stored data in multiple locations may affect Lighthouse run
*@example {IndexedDB, WebSQL} PH1
*/
thereMayBeStoredDataAffectingLoadingPlural:
'There may be stored data affecting loading performance in these locations: {PH1}. Audit this page in an incognito window to prevent those resources from affecting your scores.',
/**
*@description Help text in Lighthouse Controller
*/
multipleTabsAreBeingControlledBy:
'Multiple tabs are being controlled by the same `service worker`. Close your other tabs on the same origin to audit this page.',
/**
*@description Help text in Lighthouse Controller
*/
atLeastOneCategoryMustBeSelected: 'At least one category must be selected.',
/**
*@description Text in Application Panel Sidebar of the Application panel
*/
localStorage: 'Local Storage',
/**
*@description Text in Application Panel Sidebar of the Application panel
*/
indexeddb: 'IndexedDB',
/**
*@description Text in Application Panel Sidebar of the Application panel
*/
webSql: 'Web SQL',
/**
*@description Text of checkbox to include running the performance audits in Lighthouse
*/
performance: 'Performance',
/**
*@description Tooltip text of checkbox to include running the performance audits in Lighthouse
*/
howLongDoesThisAppTakeToShow: 'How long does this app take to show content and become usable',
/**
*@description Text of checkbox to include running the Progressive Web App audits in Lighthouse
*/
progressiveWebApp: 'Progressive Web App',
/**
*@description Tooltip text of checkbox to include running the Progressive Web App audits in Lighthouse
*/
doesThisPageMeetTheStandardOfA: 'Does this page meet the standard of a Progressive Web App',
/**
*@description Text of checkbox to include running the Best Practices audits in Lighthouse
*/
bestPractices: 'Best practices',
/**
*@description Tooltip text of checkbox to include running the Best Practices audits in Lighthouse
*/
doesThisPageFollowBestPractices: 'Does this page follow best practices for modern web development',
/**
*@description Text of checkbox to include running the Accessibility audits in Lighthouse
*/
accessibility: 'Accessibility',
/**
*@description Tooltip text of checkbox to include running the Accessibility audits in Lighthouse
*/
isThisPageUsableByPeopleWith: 'Is this page usable by people with disabilities or impairments',
/**
*@description Text of checkbox to include running the Search Engine Optimization audits in Lighthouse
*/
seo: 'SEO',
/**
*@description Tooltip text of checkbox to include running the Search Engine Optimization audits in Lighthouse
*/
isThisPageOptimizedForSearch: 'Is this page optimized for search engine results ranking',
/**
*@description Text of checkbox to include running the Ad speed and quality audits in Lighthouse
*/
publisherAds: 'Publisher Ads',
/**
*@description Tooltip text of checkbox to include running the Ad speed and quality audits in Lighthouse
*/
isThisPageOptimizedForAdSpeedAnd: 'Is this page optimized for ad speed and quality',
/**
*@description Text of checkbox to emulate mobile device behavior when running audits in Lighthouse
*/
applyMobileEmulation: 'Apply mobile emulation',
/**
*@description Tooltip text of checkbox to emulate mobile device behavior when running audits in Lighthouse
*/
applyMobileEmulationDuring: 'Apply mobile emulation during auditing',
/**
*@description Text for the mobile platform, as opposed to desktop
*/
mobile: 'Mobile',
/**
*@description Text for the desktop platform, as opposed to mobile
*/
desktop: 'Desktop',
/**
*@description Text for option to enable simulated throttling in Lighthouse Panel
*/
simulatedThrottling: 'Simulated throttling',
/**
*@description Tooltip text that appears when hovering over the 'Simulated Throttling' checkbox in the settings pane opened by clicking the setting cog in the start view of the audits panel
*/
simulateASlowerPageLoadBasedOn:
'Simulate a slower page load, based on data from an initial unthrottled load. If disabled, the page is actually slowed with applied throttling.',
/**
*@description Text of checkbox to reset storage features prior to running audits in Lighthouse
*/
clearStorage: 'Clear storage',
/**
*@description Tooltip text of checkbox to reset storage features prior to running audits in Lighthouse
*/
resetStorageLocalstorage:
'Reset storage (localStorage, IndexedDB, etc) before auditing. (Good for performance & PWA testing)',
};
const str_ = i18n.i18n.registerUIStrings('lighthouse/LighthouseController.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class LighthouseController extends Common.ObjectWrapper.ObjectWrapper implements
SDK.SDKModel.SDKModelObserver<SDK.ServiceWorkerManager.ServiceWorkerManager> {
_manager?: SDK.ServiceWorkerManager.ServiceWorkerManager|null;
_serviceWorkerListeners?: Common.EventTarget.EventDescriptor[];
_inspectedURL?: string;
constructor(protocolService: ProtocolService) {
super();
protocolService.registerStatusCallback(
message => this.dispatchEventToListeners(Events.AuditProgressChanged, {message}));
for (const preset of Presets) {
preset.setting.addChangeListener(this.recomputePageAuditability.bind(this));
}
for (const runtimeSetting of RuntimeSettings) {
runtimeSetting.setting.addChangeListener(this.recomputePageAuditability.bind(this));
}
SDK.SDKModel.TargetManager.instance().observeModels(SDK.ServiceWorkerManager.ServiceWorkerManager, this);
SDK.SDKModel.TargetManager.instance().addEventListener(
SDK.SDKModel.Events.InspectedURLChanged, this.recomputePageAuditability, this);
}
modelAdded(serviceWorkerManager: SDK.ServiceWorkerManager.ServiceWorkerManager): void {
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();
}
modelRemoved(serviceWorkerManager: SDK.ServiceWorkerManager.ServiceWorkerManager): void {
if (this._manager !== serviceWorkerManager) {
return;
}
if (this._serviceWorkerListeners) {
Common.EventTarget.EventTarget.removeEventListeners(this._serviceWorkerListeners);
}
this._manager = null;
this.recomputePageAuditability();
}
_hasActiveServiceWorker(): boolean {
if (!this._manager) {
return false;
}
const mainTarget = this._manager.target();
if (!mainTarget) {
return false;
}
const inspectedURL = Common.ParsedURL.ParsedURL.fromString(mainTarget.inspectedURL());
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;
}
_hasAtLeastOneCategory(): boolean {
return Presets.some(preset => preset.setting.get());
}
_unauditablePageMessage(): string|null {
if (!this._manager) {
return null;
}
const mainTarget = this._manager.target();
const inspectedURL = mainTarget && mainTarget.inspectedURL();
if (inspectedURL && !/^(http|chrome-extension)/.test(inspectedURL)) {
return i18nString(UIStrings.canOnlyAuditHttphttpsPagesAnd);
}
return null;
}
async _hasImportantResourcesNotCleared(): Promise<string> {
const clearStorageSetting =
RuntimeSettings.find(runtimeSetting => runtimeSetting.setting.name === 'lighthouse.clear_storage');
if (clearStorageSetting && !clearStorageSetting.setting.get()) {
return '';
}
if (!this._manager) {
return '';
}
const mainTarget = this._manager.target();
const usageData = await mainTarget.storageAgent().invoke_getUsageAndQuota({origin: mainTarget.inspectedURL()});
const locations = usageData.usageBreakdown.filter(usage => usage.usage)
.map(usage => STORAGE_TYPE_NAMES.get(usage.storageType))
.filter(Boolean);
if (locations.length === 1) {
return i18nString(UIStrings.thereMayBeStoredDataAffectingSingular, {PH1: locations[0]});
}
if (locations.length > 1) {
return i18nString(UIStrings.thereMayBeStoredDataAffectingLoadingPlural, {PH1: locations.join(', ')});
}
return '';
}
async _evaluateInspectedURL(): Promise<string> {
if (!this._manager) {
return '';
}
const mainTarget = this._manager.target();
const runtimeModel = mainTarget.model(SDK.RuntimeModel.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: 'lighthouse',
includeCommandLineAPI: false,
silent: false,
returnByValue: true,
generatePreview: false,
allowUnsafeEvalBlockedByCSP: undefined,
disableBreaks: undefined,
replMode: undefined,
throwOnSideEffect: undefined,
timeout: undefined,
},
/* userGesture */ false, /* awaitPromise */ false);
if ((!('exceptionDetails' in result) || !result.exceptionDetails) && 'object' in result && result.object) {
inspectedURL = result.object.value;
result.object.release();
}
} catch (err) {
console.error(err);
}
return inspectedURL;
}
getFlags(): {internalDisableDeviceScreenEmulation: boolean, emulatedFormFactor: (string|undefined)} {
const flags = {
// DevTools handles all the emulation. This tells Lighthouse to not bother with emulation.
internalDisableDeviceScreenEmulation: true,
};
for (const runtimeSetting of RuntimeSettings) {
runtimeSetting.setFlags(flags, runtimeSetting.setting.get());
}
return /** @type {{internalDisableDeviceScreenEmulation: boolean, emulatedFormFactor: (string|undefined)}} */ flags as
{
internalDisableDeviceScreenEmulation: boolean,
emulatedFormFactor: (string | undefined),
};
}
getCategoryIDs(): string[] {
const categoryIDs = [];
for (const preset of Presets) {
if (preset.setting.get()) {
categoryIDs.push(preset.configID);
}
}
return categoryIDs;
}
async getInspectedURL(options?: {force: boolean}): Promise<string> {
if (options && options.force || !this._inspectedURL) {
this._inspectedURL = await this._evaluateInspectedURL();
}
return this._inspectedURL;
}
recomputePageAuditability(): void {
const hasActiveServiceWorker = this._hasActiveServiceWorker();
const hasAtLeastOneCategory = this._hasAtLeastOneCategory();
const unauditablePageMessage = this._unauditablePageMessage();
let helpText = '';
if (hasActiveServiceWorker) {
helpText = i18nString(UIStrings.multipleTabsAreBeingControlledBy);
} else if (!hasAtLeastOneCategory) {
helpText = i18nString(UIStrings.atLeastOneCategoryMustBeSelected);
} else if (unauditablePageMessage) {
helpText = unauditablePageMessage;
}
this.dispatchEventToListeners(Events.PageAuditabilityChanged, {helpText});
this._hasImportantResourcesNotCleared().then(warning => {
this.dispatchEventToListeners(Events.PageWarningsChanged, {warning});
});
}
}
const STORAGE_TYPE_NAMES = new Map([
[Protocol.Storage.StorageType.Local_storage, i18nString(UIStrings.localStorage)],
[Protocol.Storage.StorageType.Indexeddb, i18nString(UIStrings.indexeddb)],
[Protocol.Storage.StorageType.Websql, i18nString(UIStrings.webSql)],
]);
export const Presets: Preset[] = [
// configID maps to Lighthouse's Object.keys(config.categories)[0] value
{
setting: Common.Settings.Settings.instance().createSetting('lighthouse.cat_perf', true),
configID: 'performance',
title: i18nString(UIStrings.performance),
description: i18nString(UIStrings.howLongDoesThisAppTakeToShow),
plugin: false,
},
{
setting: Common.Settings.Settings.instance().createSetting('lighthouse.cat_pwa', true),
configID: 'pwa',
title: i18nString(UIStrings.progressiveWebApp),
description: i18nString(UIStrings.doesThisPageMeetTheStandardOfA),
plugin: false,
},
{
setting: Common.Settings.Settings.instance().createSetting('lighthouse.cat_best_practices', true),
configID: 'best-practices',
title: i18nString(UIStrings.bestPractices),
description: i18nString(UIStrings.doesThisPageFollowBestPractices),
plugin: false,
},
{
setting: Common.Settings.Settings.instance().createSetting('lighthouse.cat_a11y', true),
configID: 'accessibility',
title: i18nString(UIStrings.accessibility),
description: i18nString(UIStrings.isThisPageUsableByPeopleWith),
plugin: false,
},
{
setting: Common.Settings.Settings.instance().createSetting('lighthouse.cat_seo', true),
configID: 'seo',
title: i18nString(UIStrings.seo),
description: i18nString(UIStrings.isThisPageOptimizedForSearch),
plugin: false,
},
{
setting: Common.Settings.Settings.instance().createSetting('lighthouse.cat_pubads', false),
plugin: true,
configID: 'lighthouse-plugin-publisher-ads',
title: i18nString(UIStrings.publisherAds),
description: i18nString(UIStrings.isThisPageOptimizedForAdSpeedAnd),
},
];
export type Flags = {
[flag: string]: string|boolean,
};
export const RuntimeSettings: RuntimeSetting[] = [
{
setting: Common.Settings.Settings.instance().createSetting('lighthouse.device_type', 'mobile'),
title: i18nString(UIStrings.applyMobileEmulation),
description: i18nString(UIStrings.applyMobileEmulationDuring),
setFlags: (flags: Flags, value: string|boolean): void => {
// See Audits.AuditsPanel._setupEmulationAndProtocolConnection()
flags.emulatedFormFactor = value;
},
options: [
{label: i18nString(UIStrings.mobile), value: 'mobile'},
{label: i18nString(UIStrings.desktop), value: 'desktop'},
],
learnMore: undefined,
},
{
// This setting is disabled, but we keep it around to show in the UI.
setting: Common.Settings.Settings.instance().createSetting('lighthouse.throttling', true),
title: i18nString(UIStrings.simulatedThrottling),
// We will disable this when we have a Lantern trace viewer within DevTools.
learnMore:
'https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#devtools-lighthouse-panel-throttling',
description: i18nString(UIStrings.simulateASlowerPageLoadBasedOn),
setFlags: (flags: Flags, value: string|boolean): void => {
flags.throttlingMethod = value ? 'simulate' : 'devtools';
},
options: undefined,
},
{
setting: Common.Settings.Settings.instance().createSetting('lighthouse.clear_storage', true),
title: i18nString(UIStrings.clearStorage),
description: i18nString(UIStrings.resetStorageLocalstorage),
setFlags: (flags: Flags, value: string|boolean): void => {
flags.disableStorageReset = !value;
},
options: undefined,
learnMore: undefined,
},
];
export const Events = {
PageAuditabilityChanged: Symbol('PageAuditabilityChanged'),
PageWarningsChanged: Symbol('PageWarningsChanged'),
AuditProgressChanged: Symbol('AuditProgressChanged'),
RequestLighthouseStart: Symbol('RequestLighthouseStart'),
RequestLighthouseCancel: Symbol('RequestLighthouseCancel'),
};
export interface Preset {
setting: Common.Settings.Setting<boolean>;
configID: string;
title: string;
description: string;
plugin: boolean;
}
export interface RuntimeSetting {
setting: Common.Settings.Setting<string|boolean>;
description: string;
setFlags: (flags: Flags, value: string|boolean) => void;
options?: {label: string, value: string}[];
title?: string;
learnMore?: string;
}