chrome-devtools-frontend 1.0.1006768 → 1.0.1007846

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 (47) hide show
  1. package/config/gni/devtools_grd_files.gni +5 -0
  2. package/config/gni/devtools_image_files.gni +2 -0
  3. package/extension-api/ExtensionAPI.d.ts +10 -0
  4. package/front_end/Images/src/ic_sources_authored.svg +5 -0
  5. package/front_end/Images/src/ic_sources_deployed.svg +5 -0
  6. package/front_end/core/i18n/locales/en-US.json +39 -3
  7. package/front_end/core/i18n/locales/en-XL.json +39 -3
  8. package/front_end/core/sdk/CSSFontFace.ts +8 -0
  9. package/front_end/core/sdk/DebuggerModel.ts +12 -3
  10. package/front_end/core/sdk/NetworkManager.ts +6 -2
  11. package/front_end/devtools_compatibility.js +1 -0
  12. package/front_end/entrypoints/formatter_worker/FormatterActions.ts +1 -0
  13. package/front_end/entrypoints/formatter_worker/ScopeParser.ts +12 -10
  14. package/front_end/entrypoints/formatter_worker/formatter_worker-entrypoint.ts +4 -0
  15. package/front_end/entrypoints/lighthouse_worker/LighthouseWorkerService.ts +1 -4
  16. package/front_end/legacy_test_runner/lighthouse_test_runner/lighthouse_test_runner.js +16 -0
  17. package/front_end/models/extensions/ExtensionAPI.ts +95 -12
  18. package/front_end/models/extensions/ExtensionEndpoint.ts +69 -0
  19. package/front_end/models/extensions/ExtensionServer.ts +21 -0
  20. package/front_end/models/extensions/LanguageExtensionEndpoint.ts +46 -78
  21. package/front_end/models/extensions/RecorderExtensionEndpoint.ts +43 -0
  22. package/front_end/models/extensions/RecorderPluginManager.ts +30 -0
  23. package/front_end/models/extensions/extensions.ts +2 -0
  24. package/front_end/models/formatter/FormatterWorkerPool.ts +6 -0
  25. package/front_end/models/javascript_metadata/JavaScriptMetadata.ts +13 -20
  26. package/front_end/models/javascript_metadata/NativeFunctions.js +1237 -3962
  27. package/front_end/models/source_map_scopes/NamesResolver.ts +206 -73
  28. package/front_end/models/workspace/UISourceCode.ts +7 -0
  29. package/front_end/panels/application/AppManifestView.ts +2 -1
  30. package/front_end/panels/application/components/BackForwardCacheView.ts +16 -0
  31. package/front_end/panels/browser_debugger/DOMBreakpointsSidebarPane.ts +15 -1
  32. package/front_end/panels/lighthouse/LighthouseController.ts +25 -10
  33. package/front_end/panels/lighthouse/LighthouseStartView.ts +32 -6
  34. package/front_end/panels/lighthouse/LighthouseStartViewFR.ts +70 -49
  35. package/front_end/panels/network/components/RequestHeadersView.css +36 -3
  36. package/front_end/panels/network/components/RequestHeadersView.ts +176 -3
  37. package/front_end/panels/sources/NavigatorView.ts +141 -40
  38. package/front_end/panels/sources/SourcesPanel.ts +8 -0
  39. package/front_end/panels/sources/TabbedEditorContainer.ts +2 -2
  40. package/front_end/panels/sources/sources-meta.ts +6 -0
  41. package/front_end/ui/components/text_editor/javascript.ts +12 -14
  42. package/front_end/ui/legacy/Treeoutline.ts +5 -2
  43. package/package.json +1 -1
  44. package/scripts/hosted_mode/server.js +14 -1
  45. package/scripts/javascript_natives/helpers.js +26 -7
  46. package/scripts/javascript_natives/index.js +4 -3
  47. package/scripts/javascript_natives/tests.js +2 -2
@@ -2,7 +2,6 @@
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 * as Platform from '../../core/platform/platform.js';
6
5
  import * as SDK from '../../core/sdk/sdk.js';
7
6
  import * as Bindings from '../bindings/bindings.js';
8
7
  import * as Formatter from '../formatter/formatter.js';
@@ -12,7 +11,7 @@ import * as Protocol from '../../generated/protocol.js';
12
11
 
13
12
  interface CachedScopeMap {
14
13
  sourceMap: SDK.SourceMap.SourceMap|null;
15
- identifiersPromise: Promise<Map<string, string>>;
14
+ mappingPromise: Promise<{variableMapping: Map<string, string>, thisMapping: string|null}>;
16
15
  }
17
16
 
18
17
  const scopeToCachedIdentifiersMap = new WeakMap<SDK.DebuggerModel.ScopeChainEntry, CachedScopeMap>();
@@ -29,61 +28,138 @@ export class Identifier {
29
28
  }
30
29
  }
31
30
 
32
- export const scopeIdentifiers = async function(scope: SDK.DebuggerModel.ScopeChainEntry): Promise<Identifier[]> {
33
- if (scope.type() === Protocol.Debugger.ScopeType.Global) {
34
- return [];
35
- }
36
- const startLocation = scope.startLocation();
37
- const endLocation = scope.endLocation();
38
- if (!startLocation || !endLocation) {
39
- return [];
31
+ const computeScopeTree = async function(functionScope: SDK.DebuggerModel.ScopeChainEntry): Promise<{
32
+ scopeTree: Formatter.FormatterWorkerPool.ScopeTreeNode, text: TextUtils.Text.Text, slide: number,
33
+ }|null> {
34
+ const functionStartLocation = functionScope.startLocation();
35
+ const functionEndLocation = functionScope.endLocation();
36
+ if (!functionStartLocation || !functionEndLocation) {
37
+ return null;
40
38
  }
41
- const script = startLocation.script();
42
- if (!script || !script.sourceMapURL || script !== endLocation.script()) {
43
- return [];
39
+ const script = functionStartLocation.script();
40
+ if (!script || !script.sourceMapURL || script !== functionEndLocation.script()) {
41
+ return null;
44
42
  }
45
43
  const {content} = await script.requestContent();
46
44
  if (!content) {
47
- return [];
45
+ return null;
48
46
  }
49
47
 
50
48
  const text = new TextUtils.Text.Text(content);
51
49
  const scopeRange = new TextUtils.TextRange.TextRange(
52
- startLocation.lineNumber, startLocation.columnNumber, endLocation.lineNumber, endLocation.columnNumber);
50
+ functionStartLocation.lineNumber, functionStartLocation.columnNumber, functionEndLocation.lineNumber,
51
+ functionEndLocation.columnNumber);
53
52
  const scopeText = text.extract(scopeRange);
54
53
  const scopeStart = text.toSourceRange(scopeRange).offset;
55
54
  const prefix = 'function fui';
56
- const identifiers =
57
- await Formatter.FormatterWorkerPool.formatterWorkerPool().javaScriptIdentifiers(prefix + scopeText);
58
- const result = [];
59
- const cursor = new TextUtils.TextCursor.TextCursor(text.lineEndings());
60
- for (const id of identifiers) {
61
- if (id.offset < prefix.length) {
62
- continue;
63
- }
64
- const start = scopeStart + id.offset - prefix.length;
65
- cursor.resetTo(start);
66
- result.push(new Identifier(id.name, cursor.lineNumber(), cursor.columnNumber()));
55
+ const scopeTree = await Formatter.FormatterWorkerPool.formatterWorkerPool().javaScriptScopeTree(prefix + scopeText);
56
+ if (!scopeTree) {
57
+ return null;
67
58
  }
68
- return result;
59
+ return {scopeTree, text, slide: scopeStart - prefix.length};
69
60
  };
70
61
 
71
- export const resolveScopeChain =
72
- async function(callFrame: SDK.DebuggerModel.CallFrame|null): Promise<SDK.DebuggerModel.ScopeChainEntry[]|null> {
73
- if (!callFrame) {
62
+ export const scopeIdentifiers = async function(
63
+ functionScope: SDK.DebuggerModel.ScopeChainEntry|null, scope: SDK.DebuggerModel.ScopeChainEntry): Promise<{
64
+ freeVariables: Identifier[], boundVariables: Identifier[],
65
+ }|null> {
66
+ if (!functionScope) {
74
67
  return null;
75
68
  }
76
- const {pluginManager} = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance();
77
- if (pluginManager) {
78
- const scopeChain = await pluginManager.resolveScopeChain(callFrame);
79
- if (scopeChain) {
80
- return scopeChain;
69
+
70
+ const startLocation = scope.startLocation();
71
+ const endLocation = scope.endLocation();
72
+ if (!startLocation || !endLocation) {
73
+ return null;
74
+ }
75
+
76
+ // Parse the function scope to get the scope tree.
77
+ const scopeTreeAndStart = await computeScopeTree(functionScope);
78
+ if (!scopeTreeAndStart) {
79
+ return null;
80
+ }
81
+ const {scopeTree, text, slide} = scopeTreeAndStart;
82
+
83
+ // Compute the offset within the scope tree coordinate space.
84
+ const scopeOffsets = {
85
+ start: text.offsetFromPosition(startLocation.lineNumber, startLocation.columnNumber) - slide,
86
+ end: text.offsetFromPosition(endLocation.lineNumber, endLocation.columnNumber) - slide,
87
+ };
88
+
89
+ if (!contains(scopeTree, scopeOffsets)) {
90
+ return null;
91
+ }
92
+
93
+ // Find the corresponding scope in the scope tree.
94
+ let containingScope = scopeTree;
95
+ const ancestorScopes = [];
96
+ while (true) {
97
+ let childFound = false;
98
+ for (const child of containingScope.children) {
99
+ if (contains(child, scopeOffsets)) {
100
+ // We found a nested containing scope, continue with search there.
101
+ ancestorScopes.push(containingScope);
102
+ containingScope = child;
103
+ childFound = true;
104
+ break;
105
+ }
106
+ // Sanity check: |scope| should not straddle any of the scopes in the tree. That is:
107
+ // Either |scope| is disjoint from |child| or |child| must be inside |scope|.
108
+ // (Or the |scope| is inside |child|, but that case is covered above.)
109
+ if (!disjoint(scopeOffsets, child) && !contains(scopeOffsets, child)) {
110
+ console.error('Wrong nesting of scopes');
111
+ return null;
112
+ }
113
+ }
114
+ if (!childFound) {
115
+ // We found the deepest scope in the tree that contains our scope chain entry.
116
+ break;
81
117
  }
82
118
  }
83
- return callFrame.scopeChain();
119
+
120
+ // Now we have containing scope. Collect all the scope variables.
121
+ const boundVariables = [];
122
+ const cursor = new TextUtils.TextCursor.TextCursor(text.lineEndings());
123
+ for (const variable of containingScope.variables) {
124
+ // Skip the fixed-kind variable (i.e., 'this' or 'arguments') if we only found their "definition"
125
+ // without any uses.
126
+ if (variable.kind === Formatter.FormatterWorkerPool.DefinitionKind.Fixed && variable.offsets.length <= 1) {
127
+ continue;
128
+ }
129
+
130
+ for (const offset of variable.offsets) {
131
+ const start = offset + slide;
132
+ cursor.resetTo(start);
133
+ boundVariables.push(new Identifier(variable.name, cursor.lineNumber(), cursor.columnNumber()));
134
+ }
135
+ }
136
+
137
+ // Compute free variables by collecting all the ancestor variables that are used in |containingScope|.
138
+ const freeVariables = [];
139
+ for (const ancestor of ancestorScopes) {
140
+ for (const ancestorVariable of ancestor.variables) {
141
+ for (const offset of ancestorVariable.offsets) {
142
+ if (offset >= containingScope.start && offset < containingScope.end) {
143
+ const start = offset + slide;
144
+ cursor.resetTo(start);
145
+ freeVariables.push(new Identifier(ancestorVariable.name, cursor.lineNumber(), cursor.columnNumber()));
146
+ }
147
+ }
148
+ }
149
+ }
150
+ return {boundVariables, freeVariables};
151
+
152
+ function contains(scope: {start: number, end: number}, candidate: {start: number, end: number}): boolean {
153
+ return (scope.start <= candidate.start) && (scope.end >= candidate.end);
154
+ }
155
+ function disjoint(scope: {start: number, end: number}, other: {start: number, end: number}): boolean {
156
+ return (scope.end <= other.start) || (other.end <= scope.start);
157
+ }
84
158
  };
85
159
 
86
- export const resolveScope = async(scope: SDK.DebuggerModel.ScopeChainEntry): Promise<Map<string, string>> => {
160
+ const resolveScope =
161
+ async(scope: SDK.DebuggerModel
162
+ .ScopeChainEntry): Promise<{variableMapping: Map<string, string>, thisMapping: string | null}> => {
87
163
  let cachedScopeMap = scopeToCachedIdentifiersMap.get(scope);
88
164
  const script = scope.callFrame().script;
89
165
  const sourceMap = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().sourceMapForScript(script);
@@ -93,33 +169,59 @@ export const resolveScope = async(scope: SDK.DebuggerModel.ScopeChainEntry): Pro
93
169
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
170
  // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
95
171
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
- const identifiersPromise = (async(): Promise<Map<any, any>> => {
97
- const namesMapping = new Map<string, string>();
98
- if (sourceMap) {
99
- const textCache = new Map<string, TextUtils.Text.Text>();
100
- // Extract as much as possible from SourceMap and resolve
101
- // missing identifier names from SourceMap ranges.
102
- const promises = [];
103
- for (const id of await scopeIdentifiers(scope)) {
104
- const entry = sourceMap.findEntry(id.lineNumber, id.columnNumber);
105
- if (entry && entry.name) {
106
- namesMapping.set(id.name, entry.name);
107
- } else {
108
- promises.push(resolveSourceName(script, sourceMap, id, textCache).then(sourceName => {
109
- if (sourceName) {
110
- namesMapping.set(id.name, sourceName);
172
+ const identifiersPromise =
173
+ (async(): Promise<{variableMapping: Map<string, string>, thisMapping: string | null}> => {
174
+ const variableMapping = new Map<string, string>();
175
+ let thisMapping = null;
176
+
177
+ if (!sourceMap) {
178
+ return {variableMapping, thisMapping};
179
+ }
180
+ const textCache = new Map<string, TextUtils.Text.Text>();
181
+ // Extract as much as possible from SourceMap and resolve
182
+ // missing identifier names from SourceMap ranges.
183
+ const promises: Promise<void>[] = [];
184
+
185
+ const resolveEntry = (id: Identifier, handler: (sourceName: string) => void): void => {
186
+ const entry = sourceMap.findEntry(id.lineNumber, id.columnNumber);
187
+ if (entry && entry.name) {
188
+ handler(entry.name);
189
+ } else {
190
+ promises.push(resolveSourceName(script, sourceMap, id, textCache).then(sourceName => {
191
+ if (sourceName) {
192
+ handler(sourceName);
193
+ }
194
+ }));
195
+ }
196
+ };
197
+
198
+ const functionScope = findFunctionScope();
199
+ const parsedVariables = await scopeIdentifiers(functionScope, scope);
200
+ if (!parsedVariables) {
201
+ return {variableMapping, thisMapping};
202
+ }
203
+ for (const id of parsedVariables.boundVariables) {
204
+ resolveEntry(id, sourceName => {
205
+ // Let use ignore 'this' mappings - those are handled separately.
206
+ if (sourceName !== 'this') {
207
+ variableMapping.set(id.name, sourceName);
111
208
  }
112
- }));
209
+ });
113
210
  }
114
- }
115
- await Promise.all(promises).then(getScopeResolvedForTest());
116
- }
117
- return namesMapping;
118
- })();
119
- cachedScopeMap = {sourceMap, identifiersPromise};
120
- scopeToCachedIdentifiersMap.set(scope, {sourceMap, identifiersPromise});
211
+ for (const id of parsedVariables.freeVariables) {
212
+ resolveEntry(id, sourceName => {
213
+ if (sourceName === 'this') {
214
+ thisMapping = id.name;
215
+ }
216
+ });
217
+ }
218
+ await Promise.all(promises).then(getScopeResolvedForTest());
219
+ return {variableMapping, thisMapping};
220
+ })();
221
+ cachedScopeMap = {sourceMap, mappingPromise: identifiersPromise};
222
+ scopeToCachedIdentifiersMap.set(scope, {sourceMap, mappingPromise: identifiersPromise});
121
223
  }
122
- return await cachedScopeMap.identifiersPromise;
224
+ return await cachedScopeMap.mappingPromise;
123
225
 
124
226
  async function resolveSourceName(
125
227
  script: SDK.Script.Script, sourceMap: SDK.SourceMap.SourceMap, id: Identifier,
@@ -152,6 +254,39 @@ export const resolveScope = async(scope: SDK.DebuggerModel.ScopeChainEntry): Pro
152
254
  const originalIdentifier = text.extract(sourceTextRange).trim();
153
255
  return /[a-zA-Z0-9_$]+/.test(originalIdentifier) ? originalIdentifier : null;
154
256
  }
257
+
258
+ function findFunctionScope(): SDK.DebuggerModel.ScopeChainEntry|null {
259
+ // First find the scope in the callframe's scope chain and then find the containing function scope (closure or local).
260
+ const scopeChain = scope.callFrame().scopeChain();
261
+ let scopeIndex = 0;
262
+ for (scopeIndex; scopeIndex < scopeChain.length; scopeIndex++) {
263
+ if (scopeChain[scopeIndex] === scope) {
264
+ break;
265
+ }
266
+ }
267
+ for (scopeIndex; scopeIndex < scopeChain.length; scopeIndex++) {
268
+ const kind = scopeChain[scopeIndex].type();
269
+ if (kind === Protocol.Debugger.ScopeType.Local || kind === Protocol.Debugger.ScopeType.Closure) {
270
+ break;
271
+ }
272
+ }
273
+ return scopeIndex === scopeChain.length ? null : scopeChain[scopeIndex];
274
+ }
275
+ };
276
+
277
+ export const resolveScopeChain =
278
+ async function(callFrame: SDK.DebuggerModel.CallFrame|null): Promise<SDK.DebuggerModel.ScopeChainEntry[]|null> {
279
+ if (!callFrame) {
280
+ return null;
281
+ }
282
+ const {pluginManager} = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance();
283
+ if (pluginManager) {
284
+ const scopeChain = await pluginManager.resolveScopeChain(callFrame);
285
+ if (scopeChain) {
286
+ return scopeChain;
287
+ }
288
+ }
289
+ return callFrame.scopeChain();
155
290
  };
156
291
 
157
292
  export const allVariablesInCallFrame = async(callFrame: SDK.DebuggerModel.CallFrame): Promise<Map<string, string>> => {
@@ -163,8 +298,8 @@ export const allVariablesInCallFrame = async(callFrame: SDK.DebuggerModel.CallFr
163
298
  const scopeChain = callFrame.scopeChain();
164
299
  const nameMappings = await Promise.all(scopeChain.map(resolveScope));
165
300
  const reverseMapping = new Map<string, string>();
166
- for (const map of nameMappings) {
167
- for (const [compiledName, originalName] of map) {
301
+ for (const {variableMapping} of nameMappings) {
302
+ for (const [compiledName, originalName] of variableMapping) {
168
303
  if (originalName && !reverseMapping.has(originalName)) {
169
304
  reverseMapping.set(originalName, compiledName);
170
305
  }
@@ -234,15 +369,13 @@ export const resolveThisObject =
234
369
  return callFrame.thisObject();
235
370
  }
236
371
 
237
- const namesMapping = await resolveScope(scopeChain[0]);
238
- const thisMappings = Platform.MapUtilities.inverse(namesMapping).get('this');
239
- if (!thisMappings || thisMappings.size !== 1) {
372
+ const {thisMapping} = await resolveScope(scopeChain[0]);
373
+ if (!thisMapping) {
240
374
  return callFrame.thisObject();
241
375
  }
242
376
 
243
- const [expression] = thisMappings.values();
244
377
  const result = await callFrame.evaluate(({
245
- expression,
378
+ expression: thisMapping,
246
379
  objectGroup: 'backtrace',
247
380
  includeCommandLineAPI: false,
248
381
  silent: true,
@@ -322,7 +455,7 @@ export class RemoteObject extends SDK.RemoteObject.RemoteObject {
322
455
  async getAllProperties(accessorPropertiesOnly: boolean, generatePreview: boolean):
323
456
  Promise<SDK.RemoteObject.GetPropertiesResult> {
324
457
  const allProperties = await this.object.getAllProperties(accessorPropertiesOnly, generatePreview);
325
- const namesMapping = await resolveScope(this.scope);
458
+ const {variableMapping} = await resolveScope(this.scope);
326
459
 
327
460
  const properties = allProperties.properties;
328
461
  const internalProperties = allProperties.internalProperties;
@@ -330,7 +463,7 @@ export class RemoteObject extends SDK.RemoteObject.RemoteObject {
330
463
  if (properties) {
331
464
  for (let i = 0; i < properties.length; ++i) {
332
465
  const property = properties[i];
333
- const name = namesMapping.get(property.name) || properties[i].name;
466
+ const name = variableMapping.get(property.name) || properties[i].name;
334
467
  if (!property.value) {
335
468
  continue;
336
469
  }
@@ -343,7 +476,7 @@ export class RemoteObject extends SDK.RemoteObject.RemoteObject {
343
476
  }
344
477
 
345
478
  async setPropertyValue(argumentName: string|Protocol.Runtime.CallArgument, value: string): Promise<string|undefined> {
346
- const namesMapping = await resolveScope(this.scope);
479
+ const {variableMapping} = await resolveScope(this.scope);
347
480
 
348
481
  let name;
349
482
  if (typeof argumentName === 'string') {
@@ -353,8 +486,8 @@ export class RemoteObject extends SDK.RemoteObject.RemoteObject {
353
486
  }
354
487
 
355
488
  let actualName: string = name;
356
- for (const compiledName of namesMapping.keys()) {
357
- if (namesMapping.get(compiledName) === name) {
489
+ for (const compiledName of variableMapping.keys()) {
490
+ if (variableMapping.get(compiledName) === name) {
358
491
  actualName = compiledName;
359
492
  break;
360
493
  }
@@ -127,6 +127,13 @@ export class UISourceCode extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
127
127
  return this.urlInternal;
128
128
  }
129
129
 
130
+ // Identifier used for deduplicating scripts that are considered by the
131
+ // DevTools UI to be the same script. For now this is just the url but this
132
+ // is likely to change in the future.
133
+ canononicalScriptId(): string {
134
+ return this.urlInternal;
135
+ }
136
+
130
137
  parentURL(): Platform.DevToolsPath.UrlString {
131
138
  return this.parentURLInternal;
132
139
  }
@@ -663,7 +663,8 @@ export class AppManifestView extends UI.Widget.VBox implements SDK.TargetManager
663
663
  }
664
664
 
665
665
  const userPreferences = parsedManifest['user_preferences'] || {};
666
- const colorSchemeDark = userPreferences['color_scheme_dark'] || {};
666
+ const colorScheme = userPreferences['color_scheme'] || {};
667
+ const colorSchemeDark = colorScheme['dark'] || {};
667
668
  const darkThemeColorString = colorSchemeDark['theme_color'];
668
669
  const hasDarkThemeColor = typeof darkThemeColorString === 'string';
669
670
  this.darkThemeColorField.parentElement?.classList.toggle('hidden', !hasDarkThemeColor);
@@ -94,6 +94,10 @@ const UIStrings = {
94
94
  * @description Link Text about explanation of back/forward cache
95
95
  */
96
96
  learnMore: 'Learn more: back/forward cache eligibility',
97
+ /**
98
+ * @description Link Text about unload handler
99
+ */
100
+ neverUseUnload: 'Learn more: Never use unload handler',
97
101
  /**
98
102
  * @description Explanation for 'pending support' items which prevent the page from being eligible
99
103
  * for back/forward cache.
@@ -536,6 +540,17 @@ export class BackForwardCacheView extends HTMLElement {
536
540
  `;
537
541
  }
538
542
 
543
+ #maybeRenderDeepLinkToUnload(explanation: Protocol.Page.BackForwardCacheNotRestoredExplanation): LitHtml.LitTemplate {
544
+ if (explanation.reason === Protocol.Page.BackForwardCacheNotRestoredReason.UnloadHandlerExistsInMainFrame ||
545
+ explanation.reason === Protocol.Page.BackForwardCacheNotRestoredReason.UnloadHandlerExistsInSubFrame) {
546
+ return LitHtml.html`
547
+ <x-link href="https://web.dev/bfcache/#never-use-the-unload-event" class="link">
548
+ ${i18nString(UIStrings.neverUseUnload)}
549
+ </x-link>`;
550
+ }
551
+ return LitHtml.nothing;
552
+ }
553
+
539
554
  #renderReason(explanation: Protocol.Page.BackForwardCacheNotRestoredExplanation, frames: string[]|undefined):
540
555
  LitHtml.TemplateResult {
541
556
  // clang-format off
@@ -554,6 +569,7 @@ export class BackForwardCacheView extends HTMLElement {
554
569
  </div>
555
570
  <div>
556
571
  ${NotRestoredReasonDescription[explanation.reason].name()}
572
+ ${this.#maybeRenderDeepLinkToUnload(explanation)}
557
573
  ${this.#maybeRenderReasonContext(explanation)}
558
574
  </div>` :
559
575
  LitHtml.nothing}
@@ -53,6 +53,13 @@ const UIStrings = {
53
53
  */
54
54
  sS: '{PH1}: {PH2}',
55
55
  /**
56
+ *@description Text with three placeholders separated by a colon and a comma
57
+ *@example {Node removed} PH1
58
+ *@example {div#id1} PH2
59
+ *@example {checked} PH3
60
+ */
61
+ sSS: '{PH1}: {PH2}, {PH3}',
62
+ /**
56
63
  *@description Text exposed to screen readers on checked items.
57
64
  */
58
65
  checked: 'checked',
@@ -196,19 +203,26 @@ export class DOMBreakpointsSidebarPane extends UI.Widget.VBox implements
196
203
  description.textContent = breakpointTypeLabel ? breakpointTypeLabel() : null;
197
204
  const breakpointTypeText = breakpointTypeLabel ? breakpointTypeLabel() : '';
198
205
  UI.ARIAUtils.setAccessibleName(checkboxElement, breakpointTypeText);
206
+ const checkedStateText = item.enabled ? i18nString(UIStrings.checked) : i18nString(UIStrings.unchecked);
199
207
  const linkifiedNode = document.createElement('monospace');
200
208
  linkifiedNode.style.display = 'block';
201
209
  labelElement.appendChild(linkifiedNode);
202
210
  void Common.Linkifier.Linkifier.linkify(item.node, {preventKeyboardFocus: true, tooltip: undefined})
203
211
  .then(linkified => {
204
212
  linkifiedNode.appendChild(linkified);
213
+ // Give the checkbox an aria-label as it is required for all form element
205
214
  UI.ARIAUtils.setAccessibleName(
206
215
  checkboxElement, i18nString(UIStrings.sS, {PH1: breakpointTypeText, PH2: linkified.deepTextContent()}));
216
+ // The parent list element is the one that actually gets focused.
217
+ // Assign it an aria-label with complete information for the screen reader to read out properly
218
+ UI.ARIAUtils.setAccessibleName(
219
+ element,
220
+ i18nString(
221
+ UIStrings.sSS, {PH1: breakpointTypeText, PH2: linkified.deepTextContent(), PH3: checkedStateText}));
207
222
  });
208
223
 
209
224
  labelElement.appendChild(description);
210
225
 
211
- const checkedStateText = item.enabled ? i18nString(UIStrings.checked) : i18nString(UIStrings.unchecked);
212
226
  if (item === this.#highlightedBreakpoint) {
213
227
  element.classList.add('breakpoint-hit');
214
228
  UI.ARIAUtils.setDescription(element, i18nString(UIStrings.sBreakpointHit, {PH1: checkedStateText}));
@@ -146,14 +146,22 @@ const UIStrings = {
146
146
  */
147
147
  desktop: 'Desktop',
148
148
  /**
149
- *@description Text for option to enable simulated throttling in Lighthouse Panel
150
- */
151
- simulatedThrottling: 'Simulated throttling',
149
+ * @description Text for an option to select a throttling method.
150
+ */
151
+ throttlingMethod: 'Throttling method',
152
152
  /**
153
- *@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
154
- */
153
+ * @description Text for an option in a dropdown to use simulated throttling. This is the default setting.
154
+ */
155
+ simulatedThrottling: 'Simulated throttling (default)',
156
+ /**
157
+ * @description Text for an option in a dropdown to use DevTools throttling. This option should only be used by advanced users.
158
+ */
159
+ devtoolsThrottling: 'DevTools throttling (advanced)',
160
+ /**
161
+ * @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
162
+ */
155
163
  simulateASlowerPageLoadBasedOn:
156
- 'Simulate a slower page load, based on data from an initial unthrottled load. If disabled, the page is actually slowed with applied throttling.',
164
+ 'Simulated throttling simulates a slower page load based on data from an initial unthrottled load. DevTools throttling actually slows down the page.',
157
165
  /**
158
166
  *@description Text of checkbox to reset storage features prior to running audits in Lighthouse
159
167
  */
@@ -515,17 +523,24 @@ export const RuntimeSettings: RuntimeSetting[] = [
515
523
  {
516
524
  // This setting is disabled, but we keep it around to show in the UI.
517
525
  setting: Common.Settings.Settings.instance().createSetting(
518
- 'lighthouse.throttling', true, Common.Settings.SettingStorageType.Synced),
519
- title: i18nLazyString(UIStrings.simulatedThrottling),
526
+ 'lighthouse.throttling', 'simulated', Common.Settings.SettingStorageType.Synced),
527
+ title: i18nLazyString(UIStrings.throttlingMethod),
520
528
  // We will disable this when we have a Lantern trace viewer within DevTools.
521
529
  learnMore:
522
530
  'https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#devtools-lighthouse-panel-throttling' as
523
531
  Platform.DevToolsPath.UrlString,
524
532
  description: i18nLazyString(UIStrings.simulateASlowerPageLoadBasedOn),
525
533
  setFlags: (flags: Flags, value: string|boolean): void => {
526
- flags.throttlingMethod = value ? 'simulate' : 'devtools';
534
+ if (typeof value === 'string') {
535
+ flags.throttlingMethod = value;
536
+ } else {
537
+ flags.throttlingMethod = value ? 'simulate' : 'devtools';
538
+ }
527
539
  },
528
- options: undefined,
540
+ options: [
541
+ {label: i18nLazyString(UIStrings.simulatedThrottling), value: 'simulate'},
542
+ {label: i18nLazyString(UIStrings.devtoolsThrottling), value: 'devtools'},
543
+ ],
529
544
  },
530
545
  {
531
546
  setting: Common.Settings.Settings.instance().createSetting(
@@ -7,7 +7,7 @@ import type * as Common from '../../core/common/common.js';
7
7
  import * as i18n from '../../core/i18n/i18n.js';
8
8
  import * as UI from '../../ui/legacy/legacy.js';
9
9
 
10
- import type {LighthouseController} from './LighthouseController.js';
10
+ import type {LighthouseController, Preset} from './LighthouseController.js';
11
11
  import {Events, Presets, RuntimeSettings} from './LighthouseController.js';
12
12
  import {RadioSetting} from './RadioSetting.js';
13
13
 
@@ -42,10 +42,11 @@ const str_ = i18n.i18n.registerUIStrings('panels/lighthouse/LighthouseStartView.
42
42
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
43
43
  export class StartView extends UI.Widget.Widget {
44
44
  protected controller: LighthouseController;
45
- private readonly settingsToolbarInternal: UI.Toolbar.Toolbar;
45
+ protected readonly settingsToolbarInternal: UI.Toolbar.Toolbar;
46
46
  protected startButton!: HTMLButtonElement;
47
47
  protected helpText?: Element;
48
48
  protected warningText?: Element;
49
+ protected checkboxes: Array<{preset: Preset, checkbox: UI.Toolbar.ToolbarCheckbox}> = [];
49
50
  private shouldConfirm?: boolean;
50
51
 
51
52
  constructor(controller: LighthouseController) {
@@ -84,7 +85,7 @@ export class StartView extends UI.Widget.Widget {
84
85
  UI.ARIAUtils.setAccessibleName(control.element, label);
85
86
  }
86
87
 
87
- private populateRuntimeSettingAsToolbarCheckbox(settingName: string, toolbar: UI.Toolbar.Toolbar): void {
88
+ protected populateRuntimeSettingAsToolbarCheckbox(settingName: string, toolbar: UI.Toolbar.Toolbar): void {
88
89
  const runtimeSetting = RuntimeSettings.find(item => item.setting.name === settingName);
89
90
  if (!runtimeSetting || !runtimeSetting.title) {
90
91
  throw new Error(`${settingName} is not a setting with a title`);
@@ -102,6 +103,30 @@ export class StartView extends UI.Widget.Widget {
102
103
  }
103
104
  }
104
105
 
106
+ protected populateRuntimeSettingAsToolbarDropdown(settingName: string, toolbar: UI.Toolbar.Toolbar): void {
107
+ const runtimeSetting = RuntimeSettings.find(item => item.setting.name === settingName);
108
+ if (!runtimeSetting || !runtimeSetting.title) {
109
+ throw new Error(`${settingName} is not a setting with a title`);
110
+ }
111
+
112
+ const options = runtimeSetting.options?.map(option => ({label: option.label(), value: option.value})) || [];
113
+
114
+ runtimeSetting.setting.setTitle(runtimeSetting.title());
115
+ const control = new UI.Toolbar.ToolbarSettingComboBox(
116
+ options,
117
+ runtimeSetting.setting as Common.Settings.Setting<string>,
118
+ runtimeSetting.title(),
119
+ );
120
+ control.setTitle(runtimeSetting.description());
121
+ toolbar.appendToolbarItem(control);
122
+ if (runtimeSetting.learnMore) {
123
+ const link =
124
+ UI.XLink.XLink.create(runtimeSetting.learnMore, i18nString(UIStrings.learnMore), 'lighthouse-learn-more');
125
+ link.style.padding = '5px';
126
+ control.element.appendChild(link);
127
+ }
128
+ }
129
+
105
130
  protected populateFormControls(fragment: UI.Fragment.Fragment, mode?: string): void {
106
131
  // Populate the device type
107
132
  const deviceTypeFormElements = fragment.$('device-type-form-elements');
@@ -109,15 +134,16 @@ export class StartView extends UI.Widget.Widget {
109
134
 
110
135
  // Populate the categories
111
136
  const categoryFormElements = fragment.$('categories-form-elements') as HTMLElement;
112
- categoryFormElements.textContent = '';
113
137
  const pluginFormElements = fragment.$('plugins-form-elements') as HTMLElement;
114
- pluginFormElements.textContent = '';
138
+
139
+ this.checkboxes = [];
115
140
  for (const preset of Presets) {
116
141
  const formElements = preset.plugin ? pluginFormElements : categoryFormElements;
117
142
  preset.setting.setTitle(preset.title());
118
143
  const checkbox = new UI.Toolbar.ToolbarSettingCheckbox(preset.setting, preset.description());
119
144
  const row = formElements.createChild('div', 'vbox lighthouse-launcher-row');
120
145
  row.appendChild(checkbox.element);
146
+ this.checkboxes.push({preset, checkbox});
121
147
  if (mode && !preset.supportedModes.includes(mode)) {
122
148
  checkbox.setEnabled(false);
123
149
  checkbox.setIndeterminate(true);
@@ -132,7 +158,7 @@ export class StartView extends UI.Widget.Widget {
132
158
  protected render(): void {
133
159
  this.populateRuntimeSettingAsToolbarCheckbox('lighthouse.legacy_navigation', this.settingsToolbarInternal);
134
160
  this.populateRuntimeSettingAsToolbarCheckbox('lighthouse.clear_storage', this.settingsToolbarInternal);
135
- this.populateRuntimeSettingAsToolbarCheckbox('lighthouse.throttling', this.settingsToolbarInternal);
161
+ this.populateRuntimeSettingAsToolbarDropdown('lighthouse.throttling', this.settingsToolbarInternal);
136
162
 
137
163
  this.startButton = UI.UIUtils.createTextButton(
138
164
  i18nString(UIStrings.generateReport),