blob: 843e716241f32afd5629e3e9a1f3823ce53c034a [file] [log] [blame]
Patrick Hulcea087f622018-05-18 00:37:53 +00001// Copyright 2018 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @implements {SDK.SDKModelObserver<!SDK.ServiceWorkerManager>}
7 * @unrestricted
8 */
9Audits2.AuditController = class extends Common.Object {
10 constructor(protocolService) {
11 super();
12
13 protocolService.registerStatusCallback(
14 message => this.dispatchEventToListeners(Audits2.Events.AuditProgressChanged, {message}));
15
16 for (const preset of Audits2.Presets)
17 preset.setting.addChangeListener(this.recomputePageAuditability.bind(this));
18
19 SDK.targetManager.observeModels(SDK.ServiceWorkerManager, this);
20 SDK.targetManager.addEventListener(
21 SDK.TargetManager.Events.InspectedURLChanged, this.recomputePageAuditability, this);
22 }
23
24 /**
25 * @override
26 * @param {!SDK.ServiceWorkerManager} serviceWorkerManager
27 */
28 modelAdded(serviceWorkerManager) {
29 if (this._manager)
30 return;
31
32 this._manager = serviceWorkerManager;
33 this._serviceWorkerListeners = [
34 this._manager.addEventListener(
35 SDK.ServiceWorkerManager.Events.RegistrationUpdated, this.recomputePageAuditability, this),
36 this._manager.addEventListener(
37 SDK.ServiceWorkerManager.Events.RegistrationDeleted, this.recomputePageAuditability, this),
38 ];
39
40 this.recomputePageAuditability();
41 }
42
43 /**
44 * @override
45 * @param {!SDK.ServiceWorkerManager} serviceWorkerManager
46 */
47 modelRemoved(serviceWorkerManager) {
48 if (this._manager !== serviceWorkerManager)
49 return;
50
51 Common.EventTarget.removeEventListeners(this._serviceWorkerListeners);
52 this._manager = null;
53 this.recomputePageAuditability();
54 }
55
56 /**
57 * @return {boolean}
58 */
59 _hasActiveServiceWorker() {
60 if (!this._manager)
61 return false;
62
63 const mainTarget = this._manager.target();
64 if (!mainTarget)
65 return false;
66
67 const inspectedURL = mainTarget.inspectedURL().asParsedURL();
68 const inspectedOrigin = inspectedURL && inspectedURL.securityOrigin();
69 for (const registration of this._manager.registrations().values()) {
70 if (registration.securityOrigin !== inspectedOrigin)
71 continue;
72
73 for (const version of registration.versions.values()) {
74 if (version.controlledClients.length > 1)
75 return true;
76 }
77 }
78
79 return false;
80 }
81
82 /**
83 * @return {boolean}
84 */
85 _hasAtLeastOneCategory() {
86 return Audits2.Presets.some(preset => preset.setting.get());
87 }
88
89 /**
90 * @return {?string}
91 */
92 _unauditablePageMessage() {
93 if (!this._manager)
94 return null;
95
96 const mainTarget = this._manager.target();
97 const inspectedURL = mainTarget && mainTarget.inspectedURL();
98 if (inspectedURL && !/^(http|chrome-extension)/.test(inspectedURL)) {
99 return Common.UIString(
100 'Can only audit HTTP/HTTPS pages and Chrome extensions. ' +
101 'Navigate to a different page to start an audit.');
102 }
103
104 // Audits don't work on most undockable targets (extension popup pages, remote debugging, etc).
105 // However, the tests run in a content shell which is not dockable yet audits just fine,
106 // so disable this check when under test.
107 if (!Host.isUnderTest() && !Runtime.queryParam('can_dock'))
108 return Common.UIString('Can only audit tabs. Navigate to this page in a separate tab to start an audit.');
109
110 return null;
111 }
112
113 /**
114 * @return {!Promise<string>}
115 */
116 async _evaluateInspectedURL() {
117 const mainTarget = this._manager.target();
118 const runtimeModel = mainTarget.model(SDK.RuntimeModel);
119 const executionContext = runtimeModel && runtimeModel.defaultExecutionContext();
120 let inspectedURL = mainTarget.inspectedURL();
121 if (!executionContext)
122 return inspectedURL;
123
124 // Evaluate location.href for a more specific URL than inspectedURL provides so that SPA hash navigation routes
125 // will be respected and audited.
126 try {
127 const result = await executionContext.evaluate(
128 {
129 expression: 'window.location.href',
130 objectGroup: 'audits',
131 includeCommandLineAPI: false,
132 silent: false,
133 returnByValue: true,
134 generatePreview: false
135 },
136 /* userGesture */ false, /* awaitPromise */ false);
137 if (!result.exceptionDetails && result.object) {
138 inspectedURL = result.object.value;
139 result.object.release();
140 }
141 } catch (err) {
142 console.error(err);
143 }
144
145 return inspectedURL;
146 }
147
148 /**
149 * @return {!Object}
150 */
151 getFlags() {
152 const flags = {};
153 for (const runtimeSetting of Audits2.RuntimeSettings)
154 runtimeSetting.setFlags(flags, runtimeSetting.setting.get());
155 return flags;
156 }
157
158 /**
159 * @return {!Array<string>}
160 */
161 getCategoryIDs() {
162 const categoryIDs = [];
163 for (const preset of Audits2.Presets) {
164 if (preset.setting.get())
165 categoryIDs.push(preset.configID);
166 }
167 return categoryIDs;
168 }
169
170 /**
171 * @param {{force: boolean}=} options
172 * @return {!Promise<string>}
173 */
174 async getInspectedURL(options) {
175 if (options && options.force || !this._inspectedURL)
176 this._inspectedURL = await this._evaluateInspectedURL();
177 return this._inspectedURL;
178 }
179
180 recomputePageAuditability() {
181 const hasActiveServiceWorker = this._hasActiveServiceWorker();
182 const hasAtLeastOneCategory = this._hasAtLeastOneCategory();
183 const unauditablePageMessage = this._unauditablePageMessage();
184
185 let helpText = '';
186 if (hasActiveServiceWorker) {
187 helpText = Common.UIString(
188 'Multiple tabs are being controlled by the same service worker. ' +
189 'Close your other tabs on the same origin to audit this page.');
190 } else if (!hasAtLeastOneCategory) {
191 helpText = Common.UIString('At least one category must be selected.');
192 } else if (unauditablePageMessage) {
193 helpText = unauditablePageMessage;
194 }
195
196 this.dispatchEventToListeners(Audits2.Events.PageAuditabilityChanged, {helpText});
197 }
198};
199
200
Patrick Hulce05c18ce2018-05-24 00:34:56 +0000201/** @typedef {{setting: !Common.Setting, configID: string, title: string, description: string}} */
Patrick Hulcea087f622018-05-18 00:37:53 +0000202Audits2.Preset;
203
204/** @type {!Array.<!Audits2.Preset>} */
205Audits2.Presets = [
206 // configID maps to Lighthouse's Object.keys(config.categories)[0] value
207 {
208 setting: Common.settings.createSetting('audits2.cat_perf', true),
209 configID: 'performance',
210 title: 'Performance',
211 description: 'How long does this app take to show content and become usable'
212 },
213 {
214 setting: Common.settings.createSetting('audits2.cat_pwa', true),
215 configID: 'pwa',
216 title: 'Progressive Web App',
217 description: 'Does this page meet the standard of a Progressive Web App'
218 },
219 {
220 setting: Common.settings.createSetting('audits2.cat_best_practices', true),
221 configID: 'best-practices',
222 title: 'Best practices',
223 description: 'Does this page follow best practices for modern web development'
224 },
225 {
226 setting: Common.settings.createSetting('audits2.cat_a11y', true),
227 configID: 'accessibility',
228 title: 'Accessibility',
229 description: 'Is this page usable by people with disabilities or impairments'
230 },
231 {
232 setting: Common.settings.createSetting('audits2.cat_seo', true),
233 configID: 'seo',
234 title: 'SEO',
235 description: 'Is this page optimized for search engine results ranking'
236 },
237];
238
Patrick Hulce05c18ce2018-05-24 00:34:56 +0000239/** @typedef {{setting: !Common.Setting, description: string, setFlags: function(!Object, string), options: (!Array|undefined), title: (string|undefined)}} */
Patrick Hulcea087f622018-05-18 00:37:53 +0000240Audits2.RuntimeSetting;
241
242/** @type {!Array.<!Audits2.RuntimeSetting>} */
243Audits2.RuntimeSettings = [
244 {
245 setting: Common.settings.createSetting('audits2.device_type', 'mobile'),
Patrick Hulce05c18ce2018-05-24 00:34:56 +0000246 description: ls`Apply mobile emulation during auditing`,
Patrick Hulcea087f622018-05-18 00:37:53 +0000247 setFlags: (flags, value) => {
248 flags.disableDeviceEmulation = value === 'desktop';
249 },
250 options: [
Patrick Hulce05c18ce2018-05-24 00:34:56 +0000251 {label: ls`Mobile`, value: 'mobile'},
252 {label: ls`Desktop`, value: 'desktop'},
Patrick Hulcea087f622018-05-18 00:37:53 +0000253 ],
254 },
255 {
256 setting: Common.settings.createSetting('audits2.throttling', 'default'),
Patrick Hulcea087f622018-05-18 00:37:53 +0000257 setFlags: (flags, value) => {
Paul Irish8f1e33d2018-05-31 02:29:50 +0000258 switch (value) {
259 case 'devtools':
260 flags.throttlingMethod = 'devtools';
261 break;
262 case 'off':
263 flags.throttlingMethod = 'provided';
264 break;
265 default:
266 flags.throttlingMethod = 'simulate';
267 }
Patrick Hulcea087f622018-05-18 00:37:53 +0000268 },
269 options: [
Paul Irish8f1e33d2018-05-31 02:29:50 +0000270 {
271 label: ls`Simulated Fast 3G, 4x CPU Slowdown`,
272 value: 'default',
273 title: 'Throttling is simulated, resulting in faster audit runs with similar measurement accuracy'
274 },
275 {
276 label: ls`Applied Fast 3G, 4x CPU Slowdown`,
277 value: 'devtools',
278 title: 'Typical DevTools throttling, with actual traffic shaping and CPU slowdown applied'
279 },
280 {
281 label: ls`No throttling`,
282 value: 'off',
283 title: 'No network or CPU throttling used. (Useful when not evaluating performance)'
284 },
Patrick Hulcea087f622018-05-18 00:37:53 +0000285 ],
286 },
287 {
Patrick Hulce05c18ce2018-05-24 00:34:56 +0000288 setting: Common.settings.createSetting('audits2.clear_storage', true),
289 title: ls`Clear storage`,
Paul Irish8f1e33d2018-05-31 02:29:50 +0000290 description: ls`Reset storage (localStorage, IndexedDB, etc) before auditing. (Good for performance & PWA testing)`,
Patrick Hulcea087f622018-05-18 00:37:53 +0000291 setFlags: (flags, value) => {
Patrick Hulce05c18ce2018-05-24 00:34:56 +0000292 flags.disableStorageReset = !value;
Patrick Hulcea087f622018-05-18 00:37:53 +0000293 },
Patrick Hulcea087f622018-05-18 00:37:53 +0000294 },
295];
296
Trent Aptedba184a62018-05-25 02:13:48 +0000297Audits2.Events = {
298 PageAuditabilityChanged: Symbol('PageAuditabilityChanged'),
299 AuditProgressChanged: Symbol('AuditProgressChanged'),
300 RequestAuditStart: Symbol('RequestAuditStart'),
301 RequestAuditCancel: Symbol('RequestAuditCancel'),
Paul Irish8f1e33d2018-05-31 02:29:50 +0000302};