chrome-devtools-frontend 1.0.969345 → 1.0.969882

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.
@@ -4,6 +4,7 @@
4
4
 
5
5
  import * as Common from '../../core/common/common.js';
6
6
  import * as Platform from '../../core/platform/platform.js';
7
+ import * as Root from '../../core/root/root.js';
7
8
  import * as SDK from '../../core/sdk/sdk.js';
8
9
  import * as Protocol from '../../generated/protocol.js';
9
10
  import * as Workspace from '../workspace/workspace.js';
@@ -31,6 +32,7 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
31
32
  private activeInternal: boolean;
32
33
  private enabled: boolean;
33
34
  private eventDescriptors: Common.EventTarget.EventDescriptor[];
35
+ #headerOverridesMap: Map<string, HeaderOverrideWithRegex[]> = new Map();
34
36
 
35
37
  private constructor(workspace: Workspace.Workspace.WorkspaceImpl) {
36
38
  super();
@@ -363,29 +365,97 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
363
365
  }
364
366
  }
365
367
 
366
- private updateInterceptionPatterns(): void {
367
- void this.updateInterceptionThrottler.schedule(innerUpdateInterceptionPatterns.bind(this));
368
-
369
- function innerUpdateInterceptionPatterns(this: NetworkPersistenceManager): Promise<void> {
370
- if (!this.activeInternal || !this.projectInternal) {
371
- return SDK.NetworkManager.MultitargetNetworkManager.instance().setInterceptionHandlerForPatterns(
372
- [], this.interceptionHandlerBound);
368
+ async generateHeaderPatterns(uiSourceCode: Workspace.UISourceCode.UISourceCode):
369
+ Promise<{headerPatterns: Set<string>, path: string, overridesWithRegex: HeaderOverrideWithRegex[]}> {
370
+ const headerPatterns = new Set<string>();
371
+ const content = (await uiSourceCode.requestContent()).content || '';
372
+ let headerOverrides: HeaderOverride[] = [];
373
+ try {
374
+ headerOverrides = JSON.parse(content) as HeaderOverride[];
375
+ if (!headerOverrides.every(isHeaderOverride)) {
376
+ throw 'Type mismatch after parsing';
373
377
  }
374
- const patterns = new Set<string>();
375
- const indexFileName = 'index.html';
376
- for (const uiSourceCode of this.projectInternal.uiSourceCodes()) {
377
- const pattern = this.patternForFileSystemUISourceCode(uiSourceCode);
378
- patterns.add(pattern);
379
- if (pattern.endsWith('/' + indexFileName)) {
380
- patterns.add(pattern.substr(0, pattern.length - indexFileName.length));
381
- }
378
+ } catch (e) {
379
+ console.error('Failed to parse', uiSourceCode.url(), 'for locally overriding headers.');
380
+ return {headerPatterns, path: '', overridesWithRegex: []};
381
+ }
382
+ const relativePath = FileSystemWorkspaceBinding.relativePath(uiSourceCode).join('/');
383
+ const decodedPath = this.decodeLocalPathToUrlPath(relativePath).slice(0, -HEADERS_FILENAME.length);
384
+
385
+ const overridesWithRegex: HeaderOverrideWithRegex[] = [];
386
+ for (const headerOverride of headerOverrides) {
387
+ headerPatterns.add('http?://' + decodedPath + headerOverride.applyTo);
388
+
389
+ // Most servers have the concept of a "directory index", which is a
390
+ // default resource name for a request targeting a "directory", e. g.
391
+ // requesting "example.com/path/" would result in the same response as
392
+ // requesting "example.com/path/index.html". To match this behavior we
393
+ // generate an additional pattern without "index.html" as the longer
394
+ // pattern would not match against a shorter request.
395
+ const {head, tail} = extractDirectoryIndex(headerOverride.applyTo);
396
+ if (tail) {
397
+ headerPatterns.add('http?://' + decodedPath + head);
398
+
399
+ const pattern = escapeRegex(decodedPath + head) + '(' + escapeRegex(tail) + ')?';
400
+ const regex = new RegExp('^https?:\/\/' + pattern + '$');
401
+ overridesWithRegex.push({
402
+ applyToRegex: regex,
403
+ headers: headerOverride.headers,
404
+ });
405
+ } else {
406
+ const regex = new RegExp('^https?:\/\/' + escapeRegex(decodedPath + headerOverride.applyTo) + '$');
407
+ overridesWithRegex.push({
408
+ applyToRegex: regex,
409
+ headers: headerOverride.headers,
410
+ });
382
411
  }
412
+ }
413
+ return {headerPatterns, path: decodedPath, overridesWithRegex};
414
+ }
415
+
416
+ async updateInterceptionPatternsForTests(): Promise<void> {
417
+ await this.#innerUpdateInterceptionPatterns();
418
+ }
419
+
420
+ private updateInterceptionPatterns(): void {
421
+ void this.updateInterceptionThrottler.schedule(this.#innerUpdateInterceptionPatterns.bind(this));
422
+ }
383
423
 
424
+ async #innerUpdateInterceptionPatterns(): Promise<void> {
425
+ this.#headerOverridesMap.clear();
426
+ if (!this.activeInternal || !this.projectInternal) {
384
427
  return SDK.NetworkManager.MultitargetNetworkManager.instance().setInterceptionHandlerForPatterns(
385
- Array.from(patterns).map(
386
- pattern => ({urlPattern: pattern, requestStage: Protocol.Fetch.RequestStage.Response})),
387
- this.interceptionHandlerBound);
428
+ [], this.interceptionHandlerBound);
429
+ }
430
+ let patterns = new Set<string>();
431
+ for (const uiSourceCode of this.projectInternal.uiSourceCodes()) {
432
+ const pattern = this.patternForFileSystemUISourceCode(uiSourceCode);
433
+ if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.HEADER_OVERRIDES) &&
434
+ uiSourceCode.name() === HEADERS_FILENAME) {
435
+ const {headerPatterns, path, overridesWithRegex} = await this.generateHeaderPatterns(uiSourceCode);
436
+ if (headerPatterns.size > 0) {
437
+ patterns = new Set([...patterns, ...headerPatterns]);
438
+ this.#headerOverridesMap.set(path, overridesWithRegex);
439
+ }
440
+ } else {
441
+ patterns.add(pattern);
442
+ }
443
+ // Most servers have the concept of a "directory index", which is a
444
+ // default resource name for a request targeting a "directory", e. g.
445
+ // requesting "example.com/path/" would result in the same response as
446
+ // requesting "example.com/path/index.html". To match this behavior we
447
+ // generate an additional pattern without "index.html" as the longer
448
+ // pattern would not match against a shorter request.
449
+ const {head, tail} = extractDirectoryIndex(pattern);
450
+ if (tail) {
451
+ patterns.add(head);
452
+ }
388
453
  }
454
+
455
+ return SDK.NetworkManager.MultitargetNetworkManager.instance().setInterceptionHandlerForPatterns(
456
+ Array.from(patterns).map(
457
+ pattern => ({urlPattern: pattern, requestStage: Protocol.Fetch.RequestStage.Response})),
458
+ this.interceptionHandlerBound);
389
459
  }
390
460
 
391
461
  private async onUISourceCodeRemoved(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<void> {
@@ -409,7 +479,7 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
409
479
  await this.unbind(uiSourceCode);
410
480
  }
411
481
 
412
- private async setProject(project: Workspace.Workspace.Project|null): Promise<void> {
482
+ async setProject(project: Workspace.Workspace.Project|null): Promise<void> {
413
483
  if (project === this.projectInternal) {
414
484
  return;
415
485
  }
@@ -452,6 +522,49 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
452
522
  }
453
523
  }
454
524
 
525
+ mergeHeaders(baseHeaders: Protocol.Fetch.HeaderEntry[], overrideHeaders: Protocol.Network.Headers):
526
+ Protocol.Fetch.HeaderEntry[] {
527
+ const result: Protocol.Fetch.HeaderEntry[] = [];
528
+ const headerMap = new Map<string, string>();
529
+ for (const header of baseHeaders) {
530
+ headerMap.set(header.name, header.value);
531
+ }
532
+ for (const [headerName, headerValue] of Object.entries(overrideHeaders)) {
533
+ headerMap.set(headerName, headerValue);
534
+ }
535
+ headerMap.forEach((headerValue, headerName) => {
536
+ result.push({name: headerName, value: headerValue});
537
+ });
538
+ return result;
539
+ }
540
+
541
+ #maybeMergeHeadersForPathSegment(path: string, requestUrl: string, headers: Protocol.Fetch.HeaderEntry[]):
542
+ Protocol.Fetch.HeaderEntry[] {
543
+ const headerOverrides = this.#headerOverridesMap.get(path) || [];
544
+ for (const headerOverride of headerOverrides) {
545
+ if (headerOverride.applyToRegex.test(requestUrl)) {
546
+ headers = this.mergeHeaders(headers, headerOverride.headers);
547
+ }
548
+ }
549
+ return headers;
550
+ }
551
+
552
+ handleHeaderInterception(interceptedRequest: SDK.NetworkManager.InterceptedRequest): Protocol.Fetch.HeaderEntry[] {
553
+ let result: Protocol.Fetch.HeaderEntry[] = interceptedRequest.responseHeaders || [];
554
+ const urlSegments = this.encodedPathFromUrl(interceptedRequest.request.url).split('/');
555
+ // Traverse the hierarchy of overrides from the most general to the most
556
+ // specific. Check with empty string first to match overrides applying to
557
+ // all domains.
558
+ // e.g. '', 'www.example.com/', 'www.example.com/path/', ...
559
+ let path = '';
560
+ result = this.#maybeMergeHeadersForPathSegment(path, interceptedRequest.request.url, result);
561
+ for (const segment of urlSegments) {
562
+ path += segment + '/';
563
+ result = this.#maybeMergeHeadersForPathSegment(path, interceptedRequest.request.url, result);
564
+ }
565
+ return result;
566
+ }
567
+
455
568
  private async interceptionHandler(interceptedRequest: SDK.NetworkManager.InterceptedRequest): Promise<void> {
456
569
  const method = interceptedRequest.request.method;
457
570
  if (!this.activeInternal || (method !== 'GET' && method !== 'POST')) {
@@ -460,9 +573,16 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
460
573
  const proj = this.projectInternal as FileSystem;
461
574
  const path = proj.fileSystemPath() + '/' + this.encodedPathFromUrl(interceptedRequest.request.url);
462
575
  const fileSystemUISourceCode = proj.uiSourceCodeForURL(path);
463
- if (!fileSystemUISourceCode) {
576
+ let responseHeaders: Protocol.Fetch.HeaderEntry[] = [];
577
+ if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.HEADER_OVERRIDES)) {
578
+ responseHeaders = this.handleHeaderInterception(interceptedRequest);
579
+ }
580
+ if (!fileSystemUISourceCode && !responseHeaders.length) {
464
581
  return;
465
582
  }
583
+ if (!responseHeaders.length) {
584
+ responseHeaders = interceptedRequest.responseHeaders || [];
585
+ }
466
586
 
467
587
  let mimeType = '';
468
588
  if (interceptedRequest.responseHeaders) {
@@ -477,32 +597,41 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
477
597
  if (!mimeType) {
478
598
  const expectedResourceType =
479
599
  Common.ResourceType.resourceTypes[interceptedRequest.resourceType] || Common.ResourceType.resourceTypes.Other;
480
- mimeType = fileSystemUISourceCode.mimeType();
600
+ mimeType = fileSystemUISourceCode?.mimeType() || '';
481
601
  if (Common.ResourceType.ResourceType.fromMimeType(mimeType) !== expectedResourceType) {
482
602
  mimeType = expectedResourceType.canonicalMimeType();
483
603
  }
484
604
  }
485
- const project = fileSystemUISourceCode.project() as FileSystem;
486
-
487
- this.originalResponseContentPromises.set(
488
- fileSystemUISourceCode, interceptedRequest.responseBody().then(response => {
489
- if (response.error || response.content === null) {
490
- return null;
491
- }
492
- if (response.encoded) {
493
- const text = atob(response.content);
494
- const data = new Uint8Array(text.length);
495
- for (let i = 0; i < text.length; ++i) {
496
- data[i] = text.charCodeAt(i);
497
- }
498
- return new TextDecoder('utf-8').decode(data);
499
- }
500
- return response.content;
501
- }));
502
605
 
503
- const blob = await project.requestFileBlob(fileSystemUISourceCode);
504
- if (blob) {
505
- void interceptedRequest.continueRequestWithContent(new Blob([blob], {type: mimeType}));
606
+ if (fileSystemUISourceCode) {
607
+ this.originalResponseContentPromises.set(
608
+ fileSystemUISourceCode, interceptedRequest.responseBody().then(response => {
609
+ if (response.error || response.content === null) {
610
+ return null;
611
+ }
612
+ if (response.encoded) {
613
+ const text = atob(response.content);
614
+ const data = new Uint8Array(text.length);
615
+ for (let i = 0; i < text.length; ++i) {
616
+ data[i] = text.charCodeAt(i);
617
+ }
618
+ return new TextDecoder('utf-8').decode(data);
619
+ }
620
+ return response.content;
621
+ }));
622
+
623
+ const project = fileSystemUISourceCode.project() as FileSystem;
624
+ const blob = await project.requestFileBlob(fileSystemUISourceCode);
625
+ if (blob) {
626
+ void interceptedRequest.continueRequestWithContent(
627
+ new Blob([blob], {type: mimeType}), /* encoded */ false, responseHeaders);
628
+ }
629
+ } else {
630
+ const responseBody = await interceptedRequest.responseBody();
631
+ if (!responseBody.error && responseBody.content) {
632
+ void interceptedRequest.continueRequestWithContent(
633
+ new Blob([responseBody.content], {type: mimeType}), /* encoded */ true, responseHeaders);
634
+ }
506
635
  }
507
636
  }
508
637
  }
@@ -512,6 +641,8 @@ const RESERVED_FILENAMES = new Set<string>([
512
641
  'com8', 'com9', 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9',
513
642
  ]);
514
643
 
644
+ const HEADERS_FILENAME = '.headers';
645
+
515
646
  // TODO(crbug.com/1167717): Make this a const enum again
516
647
  // eslint-disable-next-line rulesdir/const_enum
517
648
  export enum Events {
@@ -521,3 +652,36 @@ export enum Events {
521
652
  export type EventTypes = {
522
653
  [Events.ProjectChanged]: Workspace.Workspace.Project|null,
523
654
  };
655
+
656
+ interface HeaderOverride {
657
+ applyTo: string;
658
+ headers: Protocol.Network.Headers;
659
+ }
660
+
661
+ interface HeaderOverrideWithRegex {
662
+ applyToRegex: RegExp;
663
+ headers: Protocol.Network.Headers;
664
+ }
665
+
666
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
667
+ function isHeaderOverride(arg: any): arg is HeaderOverride {
668
+ if (!(arg && arg.applyTo && typeof (arg.applyTo === 'string') && arg.headers && Object.keys(arg.headers).length)) {
669
+ return false;
670
+ }
671
+ return Object.values(arg.headers).every(value => typeof value === 'string');
672
+ }
673
+
674
+ export function escapeRegex(pattern: string): string {
675
+ return Platform.StringUtilities.escapeCharacters(pattern, '[]{}()\\.^$+|-,?').replaceAll('*', '.*');
676
+ }
677
+
678
+ export function extractDirectoryIndex(pattern: string): {head: string, tail?: string} {
679
+ const lastSlash = pattern.lastIndexOf('/');
680
+ const tail = lastSlash >= 0 ? pattern.slice(lastSlash + 1) : pattern;
681
+ const head = lastSlash >= 0 ? pattern.slice(0, lastSlash + 1) : '';
682
+ const regex = new RegExp('^' + escapeRegex(tail) + '$');
683
+ if (regex.test('index.html') || regex.test('index.htm') || regex.test('index.php')) {
684
+ return {head, tail};
685
+ }
686
+ return {head: pattern};
687
+ }
@@ -138,10 +138,10 @@ export class TimelineFrameModel {
138
138
  }
139
139
  this.lastBeginFrame = startTime;
140
140
 
141
- this.beginFrameQueue.addFrameIfNotExists(seqId, startTime, false);
141
+ this.beginFrameQueue.addFrameIfNotExists(seqId, startTime, false, false);
142
142
  }
143
143
 
144
- handleDroppedFrame(startTime: number, seqId: number): void {
144
+ handleDroppedFrame(startTime: number, seqId: number, isPartial: boolean): void {
145
145
  if (!this.lastFrame) {
146
146
  this.startFrame(startTime);
147
147
  }
@@ -149,8 +149,9 @@ export class TimelineFrameModel {
149
149
  // This line handles the case where no BeginFrame event is issued for
150
150
  // the dropped frame. In this situation, add a BeginFrame to the queue
151
151
  // as if it actually occurred.
152
- this.beginFrameQueue.addFrameIfNotExists(seqId, startTime, true);
152
+ this.beginFrameQueue.addFrameIfNotExists(seqId, startTime, true, isPartial);
153
153
  this.beginFrameQueue.setDropped(seqId, true);
154
+ this.beginFrameQueue.setPartial(seqId, isPartial);
154
155
  }
155
156
 
156
157
  handleDrawFrame(startTime: number, seqId: number): void {
@@ -187,6 +188,9 @@ export class TimelineFrameModel {
187
188
  if (frame.isDropped) {
188
189
  this.lastFrame.dropped = true;
189
190
  }
191
+ if (frame.isPartial) {
192
+ this.lastFrame.isPartial = true;
193
+ }
190
194
  }
191
195
  }
192
196
  this.mainFrameCommitted = false;
@@ -318,7 +322,7 @@ export class TimelineFrameModel {
318
322
  } else if (event.name === RecordType.NeedsBeginFrameChanged) {
319
323
  this.handleNeedFrameChanged(timestamp, event.args['data'] && event.args['data']['needsBeginFrame']);
320
324
  } else if (event.name === RecordType.DroppedFrame) {
321
- this.handleDroppedFrame(timestamp, event.args['frameSeqId']);
325
+ this.handleDroppedFrame(timestamp, event.args['frameSeqId'], event.args['hasPartialUpdate']);
322
326
  }
323
327
  }
324
328
 
@@ -426,6 +430,7 @@ export class TimelineFrame {
426
430
  cpuTime: number;
427
431
  idle: boolean;
428
432
  dropped: boolean;
433
+ isPartial: boolean;
429
434
  layerTree: TracingFrameLayerTree|null;
430
435
  paints: LayerPaintEvent[];
431
436
  mainFrameId: number|undefined;
@@ -439,6 +444,7 @@ export class TimelineFrame {
439
444
  this.cpuTime = 0;
440
445
  this.idle = false;
441
446
  this.dropped = false;
447
+ this.isPartial = false;
442
448
  this.layerTree = null;
443
449
  this.paints = [];
444
450
  this.mainFrameId = undefined;
@@ -545,10 +551,12 @@ class BeginFrameInfo {
545
551
  seqId: number;
546
552
  startTime: number;
547
553
  isDropped: boolean;
548
- constructor(seqId: number, startTime: number, isDropped: boolean) {
554
+ isPartial: boolean;
555
+ constructor(seqId: number, startTime: number, isDropped: boolean, isPartial: boolean) {
549
556
  this.seqId = seqId;
550
557
  this.startTime = startTime;
551
558
  this.isDropped = isDropped;
559
+ this.isPartial = isPartial;
552
560
  }
553
561
  }
554
562
 
@@ -570,9 +578,9 @@ export class TimelineFrameBeginFrameQueue {
570
578
  }
571
579
 
572
580
  // Add a BeginFrame to the queue, if it does not already exit.
573
- addFrameIfNotExists(seqId: number, startTime: number, isDropped: boolean): void {
581
+ addFrameIfNotExists(seqId: number, startTime: number, isDropped: boolean, isPartial: boolean): void {
574
582
  if (!(seqId in this.mapFrames)) {
575
- this.mapFrames[seqId] = new BeginFrameInfo(seqId, startTime, isDropped);
583
+ this.mapFrames[seqId] = new BeginFrameInfo(seqId, startTime, isDropped, isPartial);
576
584
  this.queueFrames.push(seqId);
577
585
  }
578
586
  }
@@ -584,6 +592,12 @@ export class TimelineFrameBeginFrameQueue {
584
592
  }
585
593
  }
586
594
 
595
+ setPartial(seqId: number, isPartial: boolean): void {
596
+ if (seqId in this.mapFrames) {
597
+ this.mapFrames[seqId].isPartial = isPartial;
598
+ }
599
+ }
600
+
587
601
  processPendingBeginFramesOnDrawFrame(seqId: number): BeginFrameInfo[] {
588
602
  const framesToVisualize: BeginFrameInfo[] = [];
589
603
 
@@ -19,6 +19,21 @@ const UIStrings = {
19
19
  *(https://developers.google.com/web/updates/2018/09/reportingapi#sending)
20
20
  */
21
21
  noReportsToDisplay: 'No reports to display',
22
+ /**
23
+ *@description Column header for a table displaying Reporting API reports.
24
+ *Status is one of 'Queued', 'Pending', 'MarkedForRemoval' or 'Success'.
25
+ */
26
+ status: 'Status',
27
+ /**
28
+ *@description Column header for a table displaying Reporting API reports.
29
+ *Destination is the name of the endpoint the report is being sent to.
30
+ */
31
+ destination: 'Destination',
32
+ /**
33
+ *@description Column header for a table displaying Reporting API reports.
34
+ *The column contains the timestamp of when a report was generated.
35
+ */
36
+ generatedAt: 'Generated at',
22
37
  };
23
38
  const str_ = i18n.i18n.registerUIStrings('panels/application/components/ReportsGrid.ts', UIStrings);
24
39
  export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
@@ -38,7 +53,7 @@ export class ReportsGridStatusHeader extends HTMLElement {
38
53
  // Disabled until https://crbug.com/1079231 is fixed.
39
54
  // clang-format off
40
55
  render(html`
41
- ${i18n.i18n.lockedString('Status')}
56
+ ${i18nString(UIStrings.status)}
42
57
  <x-link href="https://web.dev/reporting-api/#report-status">
43
58
  <${IconButton.Icon.Icon.litTagName} class="inline-icon" .data=${{
44
59
  iconName: 'help_outline',
@@ -93,7 +108,7 @@ export class ReportsGrid extends HTMLElement {
93
108
  },
94
109
  {
95
110
  id: 'status',
96
- title: i18n.i18n.lockedString('Status'),
111
+ title: i18nString(UIStrings.status),
97
112
  widthWeighting: 20,
98
113
  hideable: false,
99
114
  visible: true,
@@ -103,14 +118,14 @@ export class ReportsGrid extends HTMLElement {
103
118
  },
104
119
  {
105
120
  id: 'destination',
106
- title: i18n.i18n.lockedString('Destination'),
121
+ title: i18nString(UIStrings.destination),
107
122
  widthWeighting: 20,
108
123
  hideable: false,
109
124
  visible: true,
110
125
  },
111
126
  {
112
127
  id: 'timestamp',
113
- title: i18n.i18n.lockedString('Timestamp'),
128
+ title: i18nString(UIStrings.generatedAt),
114
129
  widthWeighting: 20,
115
130
  hideable: false,
116
131
  visible: true,
@@ -125,6 +125,14 @@ const UIStrings = {
125
125
  *@description Text of checkbox to reset storage features prior to running audits in Lighthouse
126
126
  */
127
127
  clearStorage: 'Clear storage',
128
+ /**
129
+ * @description Text of checkbox to use the legacy Lighthouse navigation mode
130
+ */
131
+ legacyNavigation: 'Legacy navigation',
132
+ /**
133
+ * @description Tooltip text that appears when hovering over the 'Legacy navigation' checkbox in the settings pane opened by clicking the setting cog in the start view of the audits panel
134
+ */
135
+ useLegacyNavigation: 'Audit the page using classic Lighthouse when in navigation mode.',
128
136
  /**
129
137
  * @description Tooltip text of checkbox to reset storage features prior to running audits in
130
138
  * Lighthouse. Resetting the storage clears/empties it to a neutral state.
@@ -277,7 +285,11 @@ export class LighthouseController extends Common.ObjectWrapper.ObjectWrapper<Eve
277
285
  return navigationEntry.url;
278
286
  }
279
287
 
280
- getFlags(): {internalDisableDeviceScreenEmulation: boolean, emulatedFormFactor: (string|undefined)} {
288
+ getFlags(): {
289
+ internalDisableDeviceScreenEmulation: boolean,
290
+ emulatedFormFactor: (string|undefined),
291
+ legacyNavigation: boolean,
292
+ } {
281
293
  const flags = {
282
294
  // DevTools handles all the emulation. This tells Lighthouse to not bother with emulation.
283
295
  internalDisableDeviceScreenEmulation: true,
@@ -288,6 +300,7 @@ export class LighthouseController extends Common.ObjectWrapper.ObjectWrapper<Eve
288
300
  return flags as {
289
301
  internalDisableDeviceScreenEmulation: boolean,
290
302
  emulatedFormFactor: (string | undefined),
303
+ legacyNavigation: boolean,
291
304
  };
292
305
  }
293
306
 
@@ -433,6 +446,17 @@ export const RuntimeSettings: RuntimeSetting[] = [
433
446
  options: undefined,
434
447
  learnMore: undefined,
435
448
  },
449
+ {
450
+ setting: Common.Settings.Settings.instance().createSetting(
451
+ 'lighthouse.legacy_navigation', true, Common.Settings.SettingStorageType.Synced),
452
+ title: i18nLazyString(UIStrings.legacyNavigation),
453
+ description: i18nLazyString(UIStrings.useLegacyNavigation),
454
+ setFlags: (flags: Flags, value: string|boolean): void => {
455
+ flags.legacyNavigation = value;
456
+ },
457
+ options: undefined,
458
+ learnMore: undefined,
459
+ },
436
460
  ];
437
461
 
438
462
  // TODO(crbug.com/1167717): Make this a const enum again
@@ -11,6 +11,11 @@ import type * as ReportRenderer from './LighthouseReporterTypes.js';
11
11
  let lastId = 1;
12
12
 
13
13
  export class ProtocolService {
14
+ private targetInfo?: {
15
+ mainSessionId: string,
16
+ mainTargetId: string,
17
+ mainFrameId: string,
18
+ };
14
19
  private rawConnection?: ProtocolClient.InspectorBackend.Connection;
15
20
  private lighthouseWorkerPromise?: Promise<Worker>;
16
21
  private lighthouseMessageUpdateCallback?: ((arg0: string) => void);
@@ -19,26 +24,53 @@ export class ProtocolService {
19
24
  await SDK.TargetManager.TargetManager.instance().suspendAllTargets();
20
25
  const mainTarget = SDK.TargetManager.TargetManager.instance().mainTarget();
21
26
  if (!mainTarget) {
22
- throw new Error('Unable to find main target required for LightHouse');
27
+ throw new Error('Unable to find main target required for Lighthouse');
23
28
  }
24
29
  const childTargetManager = mainTarget.model(SDK.ChildTargetManager.ChildTargetManager);
25
30
  if (!childTargetManager) {
26
- throw new Error('Unable to find child target manager required for LightHouse');
31
+ throw new Error('Unable to find child target manager required for Lighthouse');
27
32
  }
28
- this.rawConnection = await childTargetManager.createParallelConnection(message => {
33
+ const resourceTreeModel = mainTarget.model(SDK.ResourceTreeModel.ResourceTreeModel);
34
+ if (!resourceTreeModel) {
35
+ throw new Error('Unable to find resource tree model required for Lighthouse');
36
+ }
37
+ const mainFrame = resourceTreeModel.mainFrame;
38
+ if (!mainFrame) {
39
+ throw new Error('Unable to find main frame required for Lighthouse');
40
+ }
41
+
42
+ const {connection, sessionId} = await childTargetManager.createParallelConnection(message => {
29
43
  if (typeof message === 'string') {
30
44
  message = JSON.parse(message);
31
45
  }
32
46
  this.dispatchProtocolMessage(message);
33
47
  });
48
+
49
+ this.rawConnection = connection;
50
+ this.targetInfo = {
51
+ mainTargetId: await childTargetManager.getParentTargetId(),
52
+ mainFrameId: mainFrame.id,
53
+ mainSessionId: sessionId,
54
+ };
34
55
  }
35
56
 
36
57
  getLocales(): readonly string[] {
37
58
  return [i18n.DevToolsLocale.DevToolsLocale.instance().locale];
38
59
  }
39
60
 
40
- startLighthouse(auditURL: string, categoryIDs: string[], flags: Object): Promise<ReportRenderer.RunnerResult> {
41
- return this.sendWithResponse('start', {url: auditURL, categoryIDs, flags, locales: this.getLocales()});
61
+ async startLighthouse(auditURL: string, categoryIDs: string[], flags: Record<string, Object|undefined>):
62
+ Promise<ReportRenderer.RunnerResult> {
63
+ if (!this.targetInfo) {
64
+ throw new Error('Unable to get target info required for Lighthouse');
65
+ }
66
+ const mode = flags.legacyNavigation ? 'start' : 'navigate';
67
+ return this.sendWithResponse(mode, {
68
+ url: auditURL,
69
+ categoryIDs,
70
+ flags,
71
+ locales: this.getLocales(),
72
+ target: this.targetInfo,
73
+ });
42
74
  }
43
75
 
44
76
  async detach(): Promise<void> {
@@ -113,6 +113,7 @@ export class StartView extends UI.Widget.Widget {
113
113
  }
114
114
 
115
115
  private render(): void {
116
+ this.populateRuntimeSettingAsToolbarCheckbox('lighthouse.legacy_navigation', this.settingsToolbarInternal);
116
117
  this.populateRuntimeSettingAsToolbarCheckbox('lighthouse.clear_storage', this.settingsToolbarInternal);
117
118
  this.populateRuntimeSettingAsToolbarCheckbox('lighthouse.throttling', this.settingsToolbarInternal);
118
119
 
@@ -321,7 +321,7 @@ export class StatusView {
321
321
  }
322
322
 
323
323
  private getPhaseForMessage(message: string): StatusPhase|null {
324
- return StatusPhases.find(phase => message.startsWith(phase.statusMessagePrefix)) || null;
324
+ return StatusPhases.find(phase => phase.statusMessageRegex.test(message)) || null;
325
325
  }
326
326
 
327
327
  private resetProgressBarClasses(): void {
@@ -457,7 +457,7 @@ export interface StatusPhase {
457
457
  id: string;
458
458
  progressBarClass: string;
459
459
  message: () => Common.UIString.LocalizedString;
460
- statusMessagePrefix: string;
460
+ statusMessageRegex: RegExp;
461
461
  }
462
462
 
463
463
  export const StatusPhases: StatusPhase[] = [
@@ -465,19 +465,19 @@ export const StatusPhases: StatusPhase[] = [
465
465
  id: 'loading',
466
466
  progressBarClass: 'loading',
467
467
  message: i18nLazyString(UIStrings.lighthouseIsLoadingThePage),
468
- statusMessagePrefix: 'Loading page',
468
+ statusMessageRegex: /^(Loading page|Navigating to)/,
469
469
  },
470
470
  {
471
471
  id: 'gathering',
472
472
  progressBarClass: 'gathering',
473
473
  message: i18nLazyString(UIStrings.lighthouseIsGatheringInformation),
474
- statusMessagePrefix: 'Gathering',
474
+ statusMessageRegex: /^(Gathering|Computing artifact)/,
475
475
  },
476
476
  {
477
477
  id: 'auditing',
478
478
  progressBarClass: 'auditing',
479
479
  message: i18nLazyString(UIStrings.almostThereLighthouseIsNow),
480
- statusMessagePrefix: 'Auditing',
480
+ statusMessageRegex: /^Auditing/,
481
481
  },
482
482
  ];
483
483