chrome-devtools-frontend 1.0.971727 → 1.0.973342

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/config/gni/devtools_grd_files.gni +2 -1
  2. package/front_end/core/i18n/locales/en-US.json +24 -0
  3. package/front_end/core/i18n/locales/en-XL.json +24 -0
  4. package/front_end/core/sdk/NetworkManager.ts +15 -7
  5. package/front_end/core/sdk/NetworkRequest.ts +16 -14
  6. package/front_end/core/sdk/ResourceTreeModel.ts +8 -10
  7. package/front_end/core/sdk/SourceMap.ts +9 -9
  8. package/front_end/entrypoints/lighthouse_worker/{LighthouseService.ts → LighthouseWorkerService.ts} +69 -38
  9. package/front_end/entrypoints/lighthouse_worker/lighthouse_worker.ts +1 -1
  10. package/front_end/models/bindings/CSSWorkspaceBinding.ts +2 -16
  11. package/front_end/models/bindings/SASSSourceMapping.ts +4 -3
  12. package/front_end/models/har/HARFormat.ts +4 -2
  13. package/front_end/models/har/Importer.ts +0 -1
  14. package/front_end/models/persistence/FileSystemWorkspaceBinding.ts +4 -4
  15. package/front_end/models/persistence/IsolatedFileSystem.ts +0 -1
  16. package/front_end/models/text_utils/StaticContentProvider.ts +5 -4
  17. package/front_end/models/workspace_diff/WorkspaceDiff.ts +20 -8
  18. package/front_end/panels/application/ServiceWorkerCacheViews.ts +2 -1
  19. package/front_end/panels/changes/ChangesView.ts +4 -4
  20. package/front_end/panels/elements/StylesSidebarPane.ts +27 -15
  21. package/front_end/panels/elements/components/LayoutPane.ts +1 -1
  22. package/front_end/panels/lighthouse/LighthouseController.ts +13 -2
  23. package/front_end/panels/lighthouse/LighthousePanel.ts +57 -8
  24. package/front_end/panels/lighthouse/LighthouseProtocolService.ts +94 -30
  25. package/front_end/panels/lighthouse/LighthouseStartView.ts +6 -2
  26. package/front_end/panels/lighthouse/LighthouseStartViewFR.ts +61 -0
  27. package/front_end/panels/lighthouse/LighthouseTimespanView.ts +99 -0
  28. package/front_end/panels/sources/NavigatorView.ts +4 -4
  29. package/front_end/third_party/codemirror.next/bundle.ts +1 -1
  30. package/front_end/third_party/codemirror.next/chunk/codemirror.js +1 -1
  31. package/front_end/third_party/codemirror.next/chunk/json.js +2 -1
  32. package/front_end/third_party/codemirror.next/codemirror.next.d.ts +28 -2
  33. package/front_end/third_party/codemirror.next/codemirror.next.js +1 -1
  34. package/front_end/third_party/codemirror.next/package.json +10 -10
  35. package/front_end/third_party/lighthouse/lighthouse-dt-bundle.js +1047 -1158
  36. package/front_end/third_party/lighthouse/locales/en-US.json +7 -1
  37. package/front_end/third_party/lighthouse/locales/en-XL.json +7 -1
  38. package/front_end/third_party/lighthouse/report/bundle.d.ts +0 -3
  39. package/front_end/third_party/lighthouse/report/bundle.js +38 -24
  40. package/front_end/third_party/lighthouse/report-assets/report-generator.mjs +1 -1
  41. package/front_end/ui/components/expandable_list/expandableList.css +1 -1
  42. package/front_end/ui/components/text_editor/config.ts +1 -0
  43. package/front_end/ui/legacy/components/source_frame/BinaryResourceViewFactory.ts +7 -4
  44. package/front_end/ui/legacy/tabbedPane.css +1 -0
  45. package/package.json +1 -1
  46. package/scripts/hosted_mode/server.js +13 -0
@@ -2,6 +2,8 @@
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
4
 
5
+ import type * as Platform from '../../core/platform/platform.js';
6
+
5
7
  /* eslint-disable @typescript-eslint/no-explicit-any */
6
8
 
7
9
  class HARBase {
@@ -197,7 +199,7 @@ export class HAREntry extends HARBase {
197
199
 
198
200
  class HARRequest extends HARBase {
199
201
  method: string;
200
- url: string;
202
+ url: Platform.DevToolsPath.UrlString;
201
203
  httpVersion: string;
202
204
  cookies: HARCookie[];
203
205
  headers: HARHeader[];
@@ -209,7 +211,7 @@ class HARRequest extends HARBase {
209
211
  constructor(data: any) {
210
212
  super(data);
211
213
  this.method = String(data['method']);
212
- this.url = String(data['url']);
214
+ this.url = String(data['url']) as Platform.DevToolsPath.UrlString;
213
215
  this.httpVersion = String(data['httpVersion']);
214
216
  this.cookies = Array.isArray(data['cookies']) ? data['cookies'].map(cookie => new HARCookie(cookie)) : [];
215
217
  this.headers = Array.isArray(data['headers']) ? data['headers'].map(header => new HARHeader(header)) : [];
@@ -38,7 +38,6 @@ export class Importer {
38
38
  lineNumber: initiatorEntry.lineNumber,
39
39
  };
40
40
  }
41
-
42
41
  const request = SDK.NetworkRequest.NetworkRequest.createWithoutBackendRequest(
43
42
  'har-' + requests.length, entry.request.url, documentURL, initiator);
44
43
  const page = pageref ? pages.get(pageref) : undefined;
@@ -79,9 +79,9 @@ export class FileSystemWorkspaceBinding {
79
79
  return fileSystem.supportsAutomapping();
80
80
  }
81
81
 
82
- static completeURL(project: Workspace.Workspace.Project, relativePath: string): string {
82
+ static completeURL(project: Workspace.Workspace.Project, relativePath: string): Platform.DevToolsPath.UrlString {
83
83
  const fsProject = project as FileSystem;
84
- return fsProject.fileSystemBaseURL + relativePath;
84
+ return Common.ParsedURL.ParsedURL.concatenate(fsProject.fileSystemBaseURL, relativePath);
85
85
  }
86
86
 
87
87
  static fileSystemPath(projectId: string): string {
@@ -155,7 +155,7 @@ export class FileSystemWorkspaceBinding {
155
155
 
156
156
  export class FileSystem extends Workspace.Workspace.ProjectStore {
157
157
  readonly fileSystemInternal: PlatformFileSystem;
158
- readonly fileSystemBaseURL: string;
158
+ readonly fileSystemBaseURL: Platform.DevToolsPath.UrlString;
159
159
  private readonly fileSystemParentURL: string;
160
160
  private readonly fileSystemWorkspaceBinding: FileSystemWorkspaceBinding;
161
161
  private readonly fileSystemPathInternal: string;
@@ -171,7 +171,7 @@ export class FileSystem extends Workspace.Workspace.ProjectStore {
171
171
  super(workspace, id, Workspace.Workspace.projectTypes.FileSystem, displayName);
172
172
 
173
173
  this.fileSystemInternal = isolatedFileSystem;
174
- this.fileSystemBaseURL = this.fileSystemInternal.path() + '/';
174
+ this.fileSystemBaseURL = Common.ParsedURL.ParsedURL.concatenate(this.fileSystemInternal.path(), '/');
175
175
  this.fileSystemParentURL = this.fileSystemBaseURL.substr(0, fileSystemPath.lastIndexOf('/') + 1);
176
176
  this.fileSystemWorkspaceBinding = fileSystemWorkspaceBinding;
177
177
  this.fileSystemPathInternal = fileSystemPath;
@@ -81,7 +81,6 @@ export class IsolatedFileSystem extends PlatformFileSystem {
81
81
 
82
82
  constructor(
83
83
  manager: IsolatedFileSystemManager, path: string, embedderPath: string, domFileSystem: FileSystem, type: string) {
84
- // TODO(crbug.com/1253323): Cast to UrlString will be removed when migration to branded types is complete.
85
84
  super(path, type);
86
85
  this.manager = manager;
87
86
  this.embedderPathInternal = embedderPath;
@@ -14,9 +14,9 @@ export class StaticContentProvider implements ContentProvider {
14
14
  private readonly lazyContent: () => Promise<DeferredContent>;
15
15
 
16
16
  constructor(
17
- contentURL: string, contentType: Common.ResourceType.ResourceType, lazyContent: () => Promise<DeferredContent>) {
18
- // TODO(crbug.com/1253323): Cast to UrlString will be removed when migration to branded types is complete.
19
- this.contentURLInternal = contentURL as Platform.DevToolsPath.UrlString;
17
+ contentURL: Platform.DevToolsPath.UrlString, contentType: Common.ResourceType.ResourceType,
18
+ lazyContent: () => Promise<DeferredContent>) {
19
+ this.contentURLInternal = contentURL;
20
20
  this.contentTypeInternal = contentType;
21
21
  this.lazyContent = lazyContent;
22
22
  }
@@ -27,7 +27,8 @@ export class StaticContentProvider implements ContentProvider {
27
27
  content: string,
28
28
  isEncoded: boolean,
29
29
  }> => Promise.resolve({content, isEncoded: false});
30
- return new StaticContentProvider(contentURL, contentType, lazyContent);
30
+ // TODO(crbug.com/1253323): Cast to UrlString will be removed when migration to branded types is complete.
31
+ return new StaticContentProvider(contentURL as Platform.DevToolsPath.UrlString, contentType, lazyContent);
31
32
  }
32
33
 
33
34
  contentURL(): Platform.DevToolsPath.UrlString {
@@ -13,6 +13,11 @@ interface DiffRequestOptions {
13
13
  shouldFormatDiff: boolean;
14
14
  }
15
15
 
16
+ interface DiffResponse {
17
+ diff: Diff.Diff.DiffArray;
18
+ formattedCurrentMapping?: FormatterModule.ScriptFormatter.FormatterSourceMapping;
19
+ }
20
+
16
21
  export class WorkspaceDiffImpl extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
17
22
  private readonly uiSourceCodeDiffs: WeakMap<Workspace.UISourceCode.UISourceCode, UISourceCodeDiff>;
18
23
  private readonly loadingUISourceCodes:
@@ -35,7 +40,7 @@ export class WorkspaceDiffImpl extends Common.ObjectWrapper.ObjectWrapper<EventT
35
40
  }
36
41
 
37
42
  requestDiff(uiSourceCode: Workspace.UISourceCode.UISourceCode, diffRequestOptions: DiffRequestOptions):
38
- Promise<Diff.Diff.DiffArray|null> {
43
+ Promise<DiffResponse|null> {
39
44
  return this.uiSourceCodeDiff(uiSourceCode).requestDiff(diffRequestOptions);
40
45
  }
41
46
 
@@ -185,7 +190,7 @@ export type EventTypes = {
185
190
 
186
191
  export class UISourceCodeDiff extends Common.ObjectWrapper.ObjectWrapper<UISourceCodeDiffEventTypes> {
187
192
  private uiSourceCode: Workspace.UISourceCode.UISourceCode;
188
- private requestDiffPromise: Promise<Diff.Diff.DiffArray|null>|null;
193
+ private requestDiffPromise: Promise<DiffResponse|null>|null;
189
194
  private pendingChanges: number|null;
190
195
  dispose: boolean;
191
196
  constructor(uiSourceCode: Workspace.UISourceCode.UISourceCode) {
@@ -218,7 +223,7 @@ export class UISourceCodeDiff extends Common.ObjectWrapper.ObjectWrapper<UISourc
218
223
  }
219
224
  }
220
225
 
221
- requestDiff(diffRequestOptions: DiffRequestOptions): Promise<Diff.Diff.DiffArray|null> {
226
+ requestDiff(diffRequestOptions: DiffRequestOptions): Promise<DiffResponse|null> {
222
227
  if (!this.requestDiffPromise) {
223
228
  this.requestDiffPromise = this.innerRequestDiff(diffRequestOptions);
224
229
  }
@@ -237,7 +242,7 @@ export class UISourceCodeDiff extends Common.ObjectWrapper.ObjectWrapper<UISourc
237
242
  return content.content || ('error' in content && content.error) || '';
238
243
  }
239
244
 
240
- private async innerRequestDiff({shouldFormatDiff}: DiffRequestOptions): Promise<Diff.Diff.DiffArray|null> {
245
+ private async innerRequestDiff({shouldFormatDiff}: DiffRequestOptions): Promise<DiffResponse|null> {
241
246
  if (this.dispose) {
242
247
  return null;
243
248
  }
@@ -270,15 +275,22 @@ export class UISourceCodeDiff extends Common.ObjectWrapper.ObjectWrapper<UISourc
270
275
  if (current === null || baseline === null) {
271
276
  return null;
272
277
  }
278
+ let formattedCurrentMapping;
273
279
  if (shouldFormatDiff) {
274
280
  baseline = (await FormatterModule.ScriptFormatter.format(
275
281
  this.uiSourceCode.contentType(), this.uiSourceCode.mimeType(), baseline))
276
282
  .formattedContent;
277
- current = (await FormatterModule.ScriptFormatter.format(
278
- this.uiSourceCode.contentType(), this.uiSourceCode.mimeType(), current))
279
- .formattedContent;
283
+ const formatCurrentResult = await FormatterModule.ScriptFormatter.format(
284
+ this.uiSourceCode.contentType(), this.uiSourceCode.mimeType(), current);
285
+ current = formatCurrentResult.formattedContent;
286
+ formattedCurrentMapping = formatCurrentResult.formattedMapping;
280
287
  }
281
- return Diff.Diff.DiffWrapper.lineDiff(baseline.split(/\r\n|\n|\r/), current.split(/\r\n|\n|\r/));
288
+ const reNewline = /\r\n?|\n/;
289
+ const diff = Diff.Diff.DiffWrapper.lineDiff(baseline.split(reNewline), current.split(reNewline));
290
+ return {
291
+ diff,
292
+ formattedCurrentMapping,
293
+ };
282
294
  }
283
295
  }
284
296
 
@@ -381,7 +381,8 @@ export class ServiceWorkerCacheView extends UI.View.SimpleView {
381
381
 
382
382
  private createRequest(entry: Protocol.CacheStorage.DataEntry): SDK.NetworkRequest.NetworkRequest {
383
383
  const request = SDK.NetworkRequest.NetworkRequest.createWithoutBackendRequest(
384
- 'cache-storage-' + entry.requestURL, entry.requestURL, '', null);
384
+ 'cache-storage-' + entry.requestURL, entry.requestURL as Platform.DevToolsPath.UrlString,
385
+ '' as Platform.DevToolsPath.UrlString, null);
385
386
  request.requestMethod = entry.requestMethod;
386
387
  request.setRequestHeaders(entry.requestHeaders);
387
388
  request.statusCode = entry.responseStatus;
@@ -173,7 +173,7 @@ export class ChangesView extends UI.Widget.VBox {
173
173
  }
174
174
 
175
175
  if (!this.selectedUISourceCode) {
176
- this.renderDiffRows(null);
176
+ this.renderDiffRows();
177
177
  return;
178
178
  }
179
179
  const uiSourceCode = this.selectedUISourceCode;
@@ -181,12 +181,12 @@ export class ChangesView extends UI.Widget.VBox {
181
181
  this.hideDiff(i18nString(UIStrings.binaryData));
182
182
  return;
183
183
  }
184
- const diff = await this.workspaceDiff.requestDiff(
184
+ const diffResponse = await this.workspaceDiff.requestDiff(
185
185
  uiSourceCode, {shouldFormatDiff: Root.Runtime.experiments.isEnabled('preciseChanges')});
186
186
  if (this.selectedUISourceCode !== uiSourceCode) {
187
187
  return;
188
188
  }
189
- this.renderDiffRows(diff);
189
+ this.renderDiffRows(diffResponse?.diff);
190
190
  }
191
191
 
192
192
  private hideDiff(message: string): void {
@@ -197,7 +197,7 @@ export class ChangesView extends UI.Widget.VBox {
197
197
  this.emptyWidget.showWidget();
198
198
  }
199
199
 
200
- private renderDiffRows(diff: Diff.Diff.DiffArray|null): void {
200
+ private renderDiffRows(diff?: Diff.Diff.DiffArray): void {
201
201
  if (!diff || (diff.length === 1 && diff[0][0] === Diff.Diff.Operation.Equal)) {
202
202
  this.hideDiff(i18nString(UIStrings.noChanges));
203
203
  } else {
@@ -1043,16 +1043,22 @@ export class StylesSidebarPane extends Common.ObjectWrapper.eventMixin<EventType
1043
1043
  if (!url) {
1044
1044
  return false;
1045
1045
  }
1046
- const changedLines = this.#urlToChangeTracker.get(url)?.changedLines;
1047
- if (!changedLines) {
1046
+ const changeTracker = this.#urlToChangeTracker.get(url);
1047
+ if (!changeTracker) {
1048
1048
  return false;
1049
1049
  }
1050
+ const {changedLines, formattedCurrentMapping} = changeTracker;
1050
1051
  const uiLocation = Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().propertyUILocation(property, true);
1051
1052
  if (!uiLocation) {
1052
1053
  return false;
1053
1054
  }
1054
- // UILocation's lineNumber starts at 0, but changedLines start at 1.
1055
- return changedLines.has(uiLocation.lineNumber + 1);
1055
+ if (!formattedCurrentMapping) {
1056
+ // UILocation's lineNumber starts at 0, but changedLines start at 1.
1057
+ return changedLines.has(uiLocation.lineNumber + 1);
1058
+ }
1059
+ const formattedLineNumber =
1060
+ formattedCurrentMapping.originalToFormatted(uiLocation.lineNumber, uiLocation.columnNumber)[0];
1061
+ return changedLines.has(formattedLineNumber + 1);
1056
1062
  }
1057
1063
 
1058
1064
  private async refreshChangedLines(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<void> {
@@ -1060,28 +1066,33 @@ export class StylesSidebarPane extends Common.ObjectWrapper.eventMixin<EventType
1060
1066
  if (!changeTracker) {
1061
1067
  return;
1062
1068
  }
1063
- const diff = await WorkspaceDiff.WorkspaceDiff.workspaceDiff().requestDiff(uiSourceCode, {shouldFormatDiff: true});
1069
+ const diffResponse =
1070
+ await WorkspaceDiff.WorkspaceDiff.workspaceDiff().requestDiff(uiSourceCode, {shouldFormatDiff: true});
1064
1071
  const changedLines = new Set<number>();
1065
- if (diff && diff.length > 0) {
1066
- const {rows} = DiffView.DiffView.buildDiffRows(diff);
1067
- for (const row of rows) {
1068
- if (row.type === DiffView.DiffView.RowType.Addition) {
1069
- changedLines.add(row.currentLineNumber);
1070
- }
1072
+ changeTracker.changedLines = changedLines;
1073
+ if (!diffResponse) {
1074
+ return;
1075
+ }
1076
+ const {diff, formattedCurrentMapping} = diffResponse;
1077
+ const {rows} = DiffView.DiffView.buildDiffRows(diff);
1078
+ for (const row of rows) {
1079
+ if (row.type === DiffView.DiffView.RowType.Addition) {
1080
+ changedLines.add(row.currentLineNumber);
1071
1081
  }
1072
1082
  }
1073
- changeTracker.changedLines = changedLines;
1083
+ changeTracker.formattedCurrentMapping = formattedCurrentMapping;
1074
1084
  }
1075
1085
 
1076
1086
  private async getFormattedChanges(): Promise<string> {
1077
1087
  let allChanges = '';
1078
1088
  for (const [url, {uiSourceCode}] of this.#urlToChangeTracker) {
1079
- const diff =
1089
+ const diffResponse =
1080
1090
  await WorkspaceDiff.WorkspaceDiff.workspaceDiff().requestDiff(uiSourceCode, {shouldFormatDiff: true});
1081
- if (!diff || diff.length < 2) {
1091
+ // Diff array with real diff will contain at least 2 lines.
1092
+ if (!diffResponse || diffResponse?.diff.length < 2) {
1082
1093
  continue;
1083
1094
  }
1084
- const changes = await formatCSSChangesFromDiff(diff);
1095
+ const changes = await formatCSSChangesFromDiff(diffResponse.diff);
1085
1096
  if (changes.length > 0) {
1086
1097
  allChanges += `/* ${escapeUrlAsCssComment(url)} */\n\n${changes}\n\n`;
1087
1098
  }
@@ -1231,6 +1242,7 @@ type ChangeTracker = {
1231
1242
  uiSourceCode: Workspace.UISourceCode.UISourceCode,
1232
1243
  changedLines: Set<number>,
1233
1244
  diffChangeCallback: () => Promise<void>,
1245
+ formattedCurrentMapping?: Formatter.ScriptFormatter.FormatterSourceMapping,
1234
1246
  };
1235
1247
 
1236
1248
  export async function formatCSSChangesFromDiff(diff: Diff.Diff.DiffArray): Promise<string> {
@@ -270,7 +270,7 @@ export class LayoutPane extends HTMLElement {
270
270
  </span>
271
271
  </label>
272
272
  <label @keyup=${onColorLabelKeyUp} @keydown=${onColorLabelKeyDown} tabindex="0" title=${i18nString(UIStrings.chooseElementOverlayColor)} class="color-picker-label" style="background: ${element.color};">
273
- <input @change=${onColorChange} @input=${onColorChange} class="color-picker" type="color" value=${element.color} />
273
+ <input @change=${onColorChange} @input=${onColorChange} tabindex="-1" class="color-picker" type="color" value=${element.color} />
274
274
  </label>
275
275
  <button tabindex="0" @click=${onElementClick} title=${i18nString(UIStrings.showElementInTheElementsPanel)} class="show-element"></button>
276
276
  </div>`;
@@ -113,11 +113,15 @@ const UIStrings = {
113
113
  */
114
114
  runLighthouseInMode: 'Run Lighthouse in navigation, timespan, or snapshot mode',
115
115
  /**
116
- * @description Text for Lighthouse navigation mode.
116
+ * @description Label of a radio option for a Lighthouse mode that audits a page navigation.
117
117
  */
118
118
  navigation: 'Navigation',
119
119
  /**
120
- * @description Text for Lighthouse snapshot mode.
120
+ * @description Label of a radio option for a Lighthouse mode that audits user interactions over a period of time.
121
+ */
122
+ timespan: 'Timespan',
123
+ /**
124
+ * @description Label of a radio option for a Lighthouse mode that audits the current page state.
121
125
  */
122
126
  snapshot: 'Snapshot',
123
127
  /**
@@ -305,6 +309,7 @@ export class LighthouseController extends Common.ObjectWrapper.ObjectWrapper<Eve
305
309
  internalDisableDeviceScreenEmulation: boolean,
306
310
  emulatedFormFactor: (string|undefined),
307
311
  legacyNavigation: boolean,
312
+ mode: string,
308
313
  } {
309
314
  const flags = {
310
315
  // DevTools handles all the emulation. This tells Lighthouse to not bother with emulation.
@@ -317,6 +322,7 @@ export class LighthouseController extends Common.ObjectWrapper.ObjectWrapper<Eve
317
322
  internalDisableDeviceScreenEmulation: boolean,
318
323
  emulatedFormFactor: (string | undefined),
319
324
  legacyNavigation: boolean,
325
+ mode: string,
320
326
  };
321
327
  }
322
328
 
@@ -447,6 +453,7 @@ export const RuntimeSettings: RuntimeSetting[] = [
447
453
  },
448
454
  options: [
449
455
  {label: i18nLazyString(UIStrings.navigation), value: 'navigation'},
456
+ {label: i18nLazyString(UIStrings.timespan), value: 'timespan'},
450
457
  {label: i18nLazyString(UIStrings.snapshot), value: 'snapshot'},
451
458
  ],
452
459
  learnMore: undefined,
@@ -495,6 +502,8 @@ export enum Events {
495
502
  PageAuditabilityChanged = 'PageAuditabilityChanged',
496
503
  PageWarningsChanged = 'PageWarningsChanged',
497
504
  AuditProgressChanged = 'AuditProgressChanged',
505
+ RequestLighthouseTimespanStart = 'RequestLighthouseTimespanStart',
506
+ RequestLighthouseTimespanEnd = 'RequestLighthouseTimespanEnd',
498
507
  RequestLighthouseStart = 'RequestLighthouseStart',
499
508
  RequestLighthouseCancel = 'RequestLighthouseCancel',
500
509
  }
@@ -515,6 +524,8 @@ export type EventTypes = {
515
524
  [Events.PageAuditabilityChanged]: PageAuditabilityChangedEvent,
516
525
  [Events.PageWarningsChanged]: PageWarningsChangedEvent,
517
526
  [Events.AuditProgressChanged]: AuditProgressChangedEvent,
527
+ [Events.RequestLighthouseTimespanStart]: boolean,
528
+ [Events.RequestLighthouseTimespanEnd]: boolean,
518
529
  [Events.RequestLighthouseStart]: boolean,
519
530
  [Events.RequestLighthouseCancel]: void,
520
531
  };
@@ -14,6 +14,7 @@ import * as Emulation from '../emulation/emulation.js';
14
14
  import type {AuditProgressChangedEvent, PageAuditabilityChangedEvent, PageWarningsChangedEvent} from './LighthouseController.js';
15
15
  import {Events, LighthouseController} from './LighthouseController.js';
16
16
  import lighthousePanelStyles from './lighthousePanel.css.js';
17
+ import type {LighthouseRun} from './LighthouseProtocolService.js';
17
18
  import {ProtocolService} from './LighthouseProtocolService.js';
18
19
 
19
20
  import type {ReportJSON, RunnerResultArtifacts} from './LighthouseReporterTypes.js';
@@ -23,6 +24,7 @@ import {Item, ReportSelector} from './LighthouseReportSelector.js';
23
24
  import {StartView} from './LighthouseStartView.js';
24
25
  import {StartViewFR} from './LighthouseStartViewFR.js';
25
26
  import {StatusView} from './LighthouseStatusView.js';
27
+ import {TimespanView} from './LighthouseTimespanView.js';
26
28
 
27
29
  const UIStrings = {
28
30
  /**
@@ -65,6 +67,7 @@ export class LighthousePanel extends UI.Panel.Panel {
65
67
  private readonly controller: LighthouseController;
66
68
  private readonly startView: StartView;
67
69
  private readonly statusView: StatusView;
70
+ private readonly timespanView: TimespanView|null;
68
71
  private warningText: Nullable<string>;
69
72
  private unauditableExplanation: Nullable<string>;
70
73
  private readonly cachedRenderedReports: Map<ReportJSON, HTMLElement>;
@@ -81,6 +84,7 @@ export class LighthousePanel extends UI.Panel.Panel {
81
84
  network: {conditions: SDK.NetworkManager.Conditions},
82
85
  };
83
86
  private isLHAttached?: boolean;
87
+ private currentLighthouseRun?: LighthouseRun;
84
88
 
85
89
  private constructor() {
86
90
  super('lighthouse');
@@ -89,8 +93,10 @@ export class LighthousePanel extends UI.Panel.Panel {
89
93
  this.controller = new LighthouseController(this.protocolService);
90
94
  if (Root.Runtime.experiments.isEnabled('lighthousePanelFR')) {
91
95
  this.startView = new StartViewFR(this.controller);
96
+ this.timespanView = new TimespanView(this.controller);
92
97
  } else {
93
98
  this.startView = new StartView(this.controller);
99
+ this.timespanView = null;
94
100
  }
95
101
  this.statusView = new StatusView(this.controller);
96
102
 
@@ -105,12 +111,10 @@ export class LighthousePanel extends UI.Panel.Panel {
105
111
  this.controller.addEventListener(Events.PageAuditabilityChanged, this.refreshStartAuditUI.bind(this));
106
112
  this.controller.addEventListener(Events.PageWarningsChanged, this.refreshWarningsUI.bind(this));
107
113
  this.controller.addEventListener(Events.AuditProgressChanged, this.refreshStatusUI.bind(this));
108
- this.controller.addEventListener(Events.RequestLighthouseStart, _event => {
109
- void this.startLighthouse();
110
- });
111
- this.controller.addEventListener(Events.RequestLighthouseCancel, _event => {
112
- void this.cancelLighthouse();
113
- });
114
+ this.controller.addEventListener(Events.RequestLighthouseTimespanStart, this.onLighthouseTimespanStart.bind(this));
115
+ this.controller.addEventListener(Events.RequestLighthouseTimespanEnd, this.onLighthouseTimespanEnd.bind(this));
116
+ this.controller.addEventListener(Events.RequestLighthouseStart, this.onLighthouseStart.bind(this));
117
+ this.controller.addEventListener(Events.RequestLighthouseCancel, this.onLighthouseCancel.bind(this));
114
118
 
115
119
  this.renderToolbar();
116
120
  this.auditResultsElement = this.contentElement.createChild('div', 'lighthouse-results-container');
@@ -132,6 +136,27 @@ export class LighthousePanel extends UI.Panel.Panel {
132
136
  return Events;
133
137
  }
134
138
 
139
+ private async onLighthouseTimespanStart(): Promise<void> {
140
+ this.timespanView?.show(this.contentElement);
141
+ await this.startLighthouse();
142
+ this.timespanView?.ready();
143
+ }
144
+
145
+ private async onLighthouseTimespanEnd(): Promise<void> {
146
+ this.timespanView?.hide();
147
+ await this.collectLighthouseResults();
148
+ }
149
+
150
+ private async onLighthouseStart(): Promise<void> {
151
+ await this.startLighthouse();
152
+ await this.collectLighthouseResults();
153
+ }
154
+
155
+ private async onLighthouseCancel(): Promise<void> {
156
+ this.timespanView?.hide();
157
+ void this.cancelLighthouse();
158
+ }
159
+
135
160
  private refreshWarningsUI(evt: Common.EventTarget.EventTargetEvent<PageWarningsChangedEvent>): void {
136
161
  // PageWarningsChanged fires multiple times during an audit, which we want to ignore.
137
162
  if (this.isLHAttached) {
@@ -148,6 +173,8 @@ export class LighthousePanel extends UI.Panel.Panel {
148
173
  return;
149
174
  }
150
175
 
176
+ this.startView.updateStartButton();
177
+
151
178
  this.unauditableExplanation = evt.data.helpText;
152
179
  this.startView.setUnauditableExplanation(evt.data.helpText);
153
180
  this.startView.setStartButtonEnabled(!evt.data.helpText);
@@ -348,11 +375,30 @@ export class LighthousePanel extends UI.Panel.Panel {
348
375
  const categoryIDs = this.controller.getCategoryIDs();
349
376
  const flags = this.controller.getFlags();
350
377
 
378
+ this.currentLighthouseRun = {inspectedURL, categoryIDs, flags};
379
+
351
380
  await this.setupEmulationAndProtocolConnection();
352
381
 
353
- this.renderStatusView(inspectedURL);
382
+ if (flags.mode === 'timespan') {
383
+ await this.protocolService.startTimespan(this.currentLighthouseRun);
384
+ }
385
+
386
+ } catch (err) {
387
+ await this.resetEmulationAndProtocolConnection();
388
+ if (err instanceof Error) {
389
+ this.statusView.renderBugReport(err);
390
+ }
391
+ }
392
+ }
393
+
394
+ private async collectLighthouseResults(): Promise<void> {
395
+ try {
396
+ if (!this.currentLighthouseRun) {
397
+ throw new Error('Lighthouse is not started');
398
+ }
399
+ this.renderStatusView(this.currentLighthouseRun.inspectedURL);
354
400
 
355
- const lighthouseResponse = await this.protocolService.startLighthouse(inspectedURL, categoryIDs, flags);
401
+ const lighthouseResponse = await this.protocolService.collectLighthouseResults(this.currentLighthouseRun);
356
402
 
357
403
  if (lighthouseResponse && lighthouseResponse.fatal) {
358
404
  const error = new Error(lighthouseResponse.message);
@@ -375,10 +421,13 @@ export class LighthousePanel extends UI.Panel.Panel {
375
421
  if (err instanceof Error) {
376
422
  this.statusView.renderBugReport(err);
377
423
  }
424
+ } finally {
425
+ this.currentLighthouseRun = undefined;
378
426
  }
379
427
  }
380
428
 
381
429
  private async cancelLighthouse(): Promise<void> {
430
+ this.currentLighthouseRun = undefined;
382
431
  this.statusView.updateStatus(i18nString(UIStrings.cancelling));
383
432
  await this.resetEmulationAndProtocolConnection();
384
433
  this.renderStartView();