chrome-devtools-frontend 1.0.1662289 → 1.0.1662965

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 (67) hide show
  1. package/.agents/skills/devtools-unicode-escaping/SKILL.md +64 -0
  2. package/.agents/skills/migrate-chromium-test/SKILL.md +29 -4
  3. package/front_end/core/common/SettingRegistration.ts +0 -5
  4. package/front_end/core/common/Settings.ts +69 -3
  5. package/front_end/core/platform/StringUtilities.ts +38 -6
  6. package/front_end/core/sdk/ServiceWorkerCacheModel.ts +0 -10
  7. package/front_end/core/sdk/StorageBucketsModel.ts +0 -10
  8. package/front_end/core/sdk/sdk-meta.ts +0 -1
  9. package/front_end/entrypoints/inspector_main/OutermostTargetSelector.ts +0 -13
  10. package/front_end/entrypoints/inspector_main/RenderingOptions.ts +8 -3
  11. package/front_end/entrypoints/inspector_main/inspector_main-meta.ts +3 -3
  12. package/front_end/entrypoints/main/MainImpl.ts +7 -2
  13. package/front_end/foundation/Universe.ts +16 -0
  14. package/front_end/generated/InspectorBackendCommands.ts +2 -14
  15. package/front_end/generated/protocol-mapping.d.ts +0 -53
  16. package/front_end/generated/protocol-proxy-api.d.ts +0 -46
  17. package/front_end/generated/protocol.ts +0 -148
  18. package/front_end/models/ai_assistance/AiAgent2.ts +3 -3
  19. package/front_end/models/ai_assistance/AiConversation.ts +21 -10
  20. package/front_end/models/ai_assistance/AiHistoryStorage.ts +21 -10
  21. package/front_end/models/ai_assistance/ChangeManager.ts +2 -2
  22. package/front_end/models/ai_assistance/ExtensionScope.ts +6 -2
  23. package/front_end/models/ai_assistance/agents/AccessibilityAgent.ts +1 -1
  24. package/front_end/models/ai_assistance/agents/AiAgent.ts +8 -1
  25. package/front_end/models/ai_assistance/agents/StylingAgent.ts +1 -1
  26. package/front_end/models/badges/UserBadges.ts +31 -17
  27. package/front_end/models/bindings/NetworkProject.ts +16 -4
  28. package/front_end/models/emulation/DeviceModeModel.ts +67 -5
  29. package/front_end/models/extensions/RecorderPluginManager.ts +10 -6
  30. package/front_end/models/har/Log.snapshot.txt +193 -0
  31. package/front_end/models/har/Log.ts +3 -2
  32. package/front_end/models/source_map_scopes/FunctionCodeResolver.ts +7 -5
  33. package/front_end/models/source_map_scopes/NamesResolver.ts +36 -30
  34. package/front_end/models/stack_trace/StackTrace.ts +17 -0
  35. package/front_end/panels/application/IndexedDBModel.ts +0 -10
  36. package/front_end/panels/application/IndexedDBViews.ts +240 -164
  37. package/front_end/panels/application/SharedStorageModel.ts +0 -10
  38. package/front_end/panels/application/components/AdsView.ts +23 -0
  39. package/front_end/panels/application/components/SharedStorageAccessGrid.ts +1 -1
  40. package/front_end/panels/application/components/adsView.css +21 -1
  41. package/front_end/panels/application/indexedDBViews.css +1 -1
  42. package/front_end/panels/network/NetworkDataGridNode.ts +43 -1
  43. package/front_end/panels/network/RequestInitiatorView.ts +24 -2
  44. package/front_end/panels/network/networkLogView.css +5 -0
  45. package/front_end/panels/network/requestInitiatorViewTree.css +5 -0
  46. package/front_end/panels/profiler/HeapProfileView.ts +198 -79
  47. package/front_end/panels/recorder/README.md +1 -2
  48. package/front_end/panels/recorder/RecorderPanel.ts +1749 -17
  49. package/front_end/panels/recorder/recorder.ts +0 -2
  50. package/front_end/panels/settings/AISettingsTab.ts +13 -0
  51. package/front_end/third_party/chromium/README.chromium +1 -1
  52. package/front_end/ui/components/spinners/Spinner.ts +29 -32
  53. package/front_end/ui/components/spinners/spinner.css +32 -84
  54. package/front_end/ui/legacy/Treeoutline.ts +14 -6
  55. package/front_end/ui/legacy/UIUtils.ts +25 -10
  56. package/front_end/ui/legacy/Widget.ts +17 -10
  57. package/front_end/ui/legacy/components/data_grid/DataGridElement.ts +36 -7
  58. package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +63 -68
  59. package/front_end/ui/legacy/components/object_ui/RemoteObjectPreviewFormatter.ts +1 -1
  60. package/front_end/ui/legacy/components/object_ui/objectValue.css +2 -1
  61. package/front_end/ui/legacy/inspectorCommon.css +4 -4
  62. package/front_end/ui/lit/lit.ts +1 -0
  63. package/front_end/ui/lit/render.ts +44 -10
  64. package/front_end/ui/lit/strip-whitespace.ts +23 -2
  65. package/package.json +1 -1
  66. package/front_end/panels/recorder/RecorderController.ts +0 -1759
  67. /package/front_end/panels/recorder/{recorderController.css → recorderPanel.css} +0 -0
@@ -1,1759 +0,0 @@
1
- // Copyright 2023 The Chromium Authors
2
- // Use of this source code is governed by a BSD-style license that can be
3
- // found in the LICENSE file.
4
- import '../../ui/kit/kit.js';
5
-
6
- import * as Common from '../../core/common/common.js';
7
- import * as Host from '../../core/host/host.js';
8
- import * as i18n from '../../core/i18n/i18n.js';
9
- import * as Platform from '../../core/platform/platform.js';
10
- import * as Root from '../../core/root/root.js';
11
- import * as SDK from '../../core/sdk/sdk.js';
12
- import * as Bindings from '../../models/bindings/bindings.js';
13
- import * as PublicExtensions from '../../models/extensions/extensions.js';
14
- import type * as Trace from '../../models/trace/trace.js';
15
- import * as PanelCommon from '../../panels/common/common.js';
16
- import * as Emulation from '../../panels/emulation/emulation.js';
17
- import * as Tracing from '../../services/tracing/tracing.js';
18
- import * as Buttons from '../../ui/components/buttons/buttons.js';
19
- import type * as Dialogs from '../../ui/components/dialogs/dialogs.js';
20
- import type * as Menus from '../../ui/components/menus/menus.js';
21
- import * as UI from '../../ui/legacy/legacy.js';
22
- import * as Lit from '../../ui/lit/lit.js';
23
- import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
24
-
25
- import * as Converters from './converters/converters.js';
26
- import {CreateRecordingView} from './CreateRecordingView.js';
27
- import * as Extensions from './extensions/extensions.js';
28
- import * as Models from './models/models.js';
29
- import * as Actions from './recorder-actions/recorder-actions.js';
30
- import recorderControllerStyles from './recorderController.css.js';
31
- import * as Events from './RecorderEvents.js';
32
- import {
33
- DeleteRecordingEvent,
34
- OpenRecordingEvent,
35
- PlayRecordingEvent as ListViewPlayRecordingEvent,
36
- RecordingListView
37
- } from './RecordingListView.js';
38
- import {
39
- type PlayRecordingEvent as ViewPlayRecordingEvent,
40
- RecordingView,
41
- type ReplayState,
42
- TargetPanel
43
- } from './RecordingView.js';
44
- import type {RequestSelectorAttributeEvent} from './SelectorPicker.js';
45
- import {
46
- type AddBreakpointEvent,
47
- type AddStep,
48
- AddStepPosition,
49
- type RemoveBreakpointEvent,
50
- type RemoveStep,
51
- type StepChanged
52
- } from './StepView.js';
53
-
54
- const {html, Directives: {ref}} = Lit;
55
-
56
- const UIStrings = {
57
- /**
58
- * @description The title of the button that leads to a page for creating a new recording.
59
- */
60
- createRecording: 'Create recording',
61
- /**
62
- * @description The title of the button that allows importing a recording.
63
- */
64
- importRecording: 'Import recording',
65
- /**
66
- * @description The announcement text for screen readers when a recording is imported.
67
- */
68
- recordingImported: 'Recording imported',
69
- /**
70
- * @description The title of the button that deletes the recording.
71
- */
72
- deleteRecording: 'Delete recording',
73
- /**
74
- * @description The announcement text for screen readers when a recording is deleted.
75
- */
76
- recordingDeleted: 'Recording deleted',
77
- /**
78
- * @description The title of the select option if the user has no saved recordings.
79
- */
80
- noRecordings: 'No recordings',
81
- /**
82
- * @description The title of the select option for one or more recording number followed by this text - 1 recording(s) or 4 recording(s).
83
- */
84
- numberOfRecordings: 'recording(s)',
85
- /**
86
- * @description The title of the button that continues the replay.
87
- */
88
- continueReplay: 'Continue',
89
- /**
90
- * @description The title of the button that executes only one step in the replay.
91
- */
92
- stepOverReplay: 'Execute one step',
93
- /**
94
- * @description The title of the button that opens a menu with various options of exporting a recording to file.
95
- */
96
- exportRecording: 'Export recording',
97
- /**
98
- * @description The title of shortcut for starting and stopping recording.
99
- */
100
- startStopRecording: 'Start/stop recording',
101
- /**
102
- * @description The title of shortcut for replaying recording.
103
- */
104
- replayRecording: 'Replay recording',
105
- /**
106
- * @description The title of shortcut for copying a recording or selected step.
107
- */
108
- copyShortcut: 'Copy recording or selected step',
109
- /**
110
- * @description The title of shortcut for toggling code view.
111
- */
112
- toggleCode: 'Toggle code view',
113
- /**
114
- * @description The title of the menu group in the export menu of the Recorder
115
- * panel that is followed by the list of built-in export formats.
116
- */
117
- export: 'Export',
118
- /**
119
- * @description The announcement text for screen readers when a recording is exported successfully.
120
- */
121
- recordingExported: 'Recording exported',
122
- /**
123
- * @description The title of the menu group in the export menu of the Recorder
124
- * panel that is followed by the list of export formats available via browser
125
- * extensions.
126
- */
127
- exportViaExtensions: 'Export via extensions',
128
- /**
129
- * @description The title of the menu option that leads to a page that lists
130
- * all browser extensions available for the Recorder panel.
131
- */
132
- getExtensions: 'Get extensions…',
133
- /**
134
- * @description The button label that leads to the feedback form for the Recorder panel.
135
- */
136
- sendFeedback: 'Send feedback',
137
- /**
138
- * @description The header of the start page in the Recorder panel.
139
- */
140
- header: 'Nothing recorded yet',
141
- /**
142
- * @description Text to explain the usage of the Recorder panel.
143
- */
144
- recordingDescription: 'Use recordings to create automated end-to-end tests or performance traces.',
145
- /**
146
- * @description Link text to forward to a documentation page on the Recorder panel.
147
- */
148
- learnMore: 'Learn more',
149
- /**
150
- * @description Headline of warning shown when users import a recording into the Recorder panel.
151
- */
152
- doYouTrustThisCode: 'Do you trust this recording?',
153
- /**
154
- * @description Warning shown to users when importing code into the Recorder panel. IMPORTANT: keep double quotes around PH1 and do not use single quotes.
155
- * @example {allow importing} PH1
156
- */
157
- doNotImport:
158
- 'Don’t import recordings you don’t understand or haven’t reviewed yourself into DevTools. This could allow attackers to steal your identity or take control of your computer. Type “{PH1}” below to allow importing.',
159
- /**
160
- * @description Text a user needs to type in order to confirm that they
161
- * are aware of the danger of importing code into the Recorder panel.
162
- */
163
- allowImporting: 'allow importing',
164
- /**
165
- * @description Input box placeholder which instructs the user to type 'allow importing' into the input box. IMPORTANT: keep double quotes around PH1 and do not use single quotes.
166
- * @example {allow importing} PH1
167
- */
168
- typeAllowImporting: 'Type “{PH1}”',
169
- } as const;
170
- const str_ = i18n.i18n.registerUIStrings('panels/recorder/RecorderController.ts', UIStrings);
171
- const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
172
- const {widget} = UI.Widget;
173
-
174
- const GET_EXTENSIONS_MENU_ITEM = 'get-extensions-link';
175
- const GET_EXTENSIONS_URL = 'https://goo.gle/recorder-extension-list' as Platform.DevToolsPath.UrlString;
176
- const RECORDER_EXPLANATION_URL = 'https://developer.chrome.com/docs/devtools/recorder';
177
- const FEEDBACK_URL = 'https://goo.gle/recorder-feedback' as Platform.DevToolsPath.UrlString;
178
-
179
- interface StoredRecording {
180
- storageName: string;
181
- flow: Models.Schema.UserFlow;
182
- }
183
-
184
- interface SetCurrentRecordingOptions {
185
- /**
186
- * Whether to keep breakpoints in the recording.
187
- */
188
- keepBreakpoints: boolean;
189
- /**
190
- * Whether to upstream the recording to a recording session if it exists.
191
- */
192
- updateSession: boolean;
193
- }
194
-
195
- export const enum Pages {
196
- START_PAGE = 'StartPage',
197
- ALL_RECORDINGS_PAGE = 'AllRecordingsPage',
198
- CREATE_RECORDING_PAGE = 'CreateRecordingPage',
199
- RECORDING_PAGE = 'RecordingPage',
200
- }
201
-
202
- const CONVERTER_ID_TO_METRIC: Record<string, Host.UserMetrics.RecordingExported|undefined> = {
203
- [Models.ConverterIds.ConverterIds.JSON]: Host.UserMetrics.RecordingExported.TO_JSON,
204
- [Models.ConverterIds.ConverterIds.REPLAY]: Host.UserMetrics.RecordingExported.TO_PUPPETEER_REPLAY,
205
- [Models.ConverterIds.ConverterIds.PUPPETEER]: Host.UserMetrics.RecordingExported.TO_PUPPETEER,
206
- [Models.ConverterIds.ConverterIds.PUPPETEER_FIREFOX]: Host.UserMetrics.RecordingExported.TO_PUPPETEER,
207
- [Models.ConverterIds.ConverterIds.LIGHTHOUSE]: Host.UserMetrics.RecordingExported.TO_LIGHTHOUSE,
208
- };
209
-
210
- /** Provide some defaults to prevent OOM issues like crbug.com/491027421 */
211
- function verifyFlowSize(flow: Models.Schema.UserFlow): void {
212
- if (flow.steps.length > 4096) {
213
- throw new Error('Recording with steps over 4096 is not allowed');
214
- }
215
-
216
- if (flow.title.length > 300) {
217
- throw new Error('Recording with title over 300 characters is not allowed');
218
- }
219
- }
220
-
221
- export class RecorderController extends UI.Widget.VBox<DocumentFragment> {
222
- #currentRecordingSession?: Models.RecordingSession.RecordingSession;
223
- get currentRecordingSession(): Models.RecordingSession.RecordingSession|undefined {
224
- return this.#currentRecordingSession;
225
- }
226
- set currentRecordingSession(value: Models.RecordingSession.RecordingSession|undefined) {
227
- if (this.#currentRecordingSession !== value) {
228
- this.#currentRecordingSession = value;
229
- this.requestUpdate();
230
- }
231
- }
232
-
233
- #currentRecording: StoredRecording|undefined;
234
- get currentRecording(): StoredRecording|undefined {
235
- return this.#currentRecording;
236
- }
237
- set currentRecording(value: StoredRecording|undefined) {
238
- if (this.#currentRecording !== value) {
239
- this.#currentRecording = value;
240
- this.requestUpdate();
241
- }
242
- }
243
-
244
- #currentStep?: Models.Schema.Step;
245
- get currentStep(): Models.Schema.Step|undefined {
246
- return this.#currentStep;
247
- }
248
- set currentStep(value: Models.Schema.Step|undefined) {
249
- if (this.#currentStep !== value) {
250
- this.#currentStep = value;
251
- this.requestUpdate();
252
- }
253
- }
254
-
255
- #recordingError?: Error;
256
- get recordingError(): Error|undefined {
257
- return this.#recordingError;
258
- }
259
- set recordingError(value: Error|undefined) {
260
- if (this.#recordingError !== value) {
261
- this.#recordingError = value;
262
- this.requestUpdate();
263
- }
264
- }
265
-
266
- #storage = Models.RecordingStorage.RecordingStorage.instance();
267
- #screenshotStorage = Models.ScreenshotStorage.ScreenshotStorage.instance();
268
-
269
- #isRecording = false;
270
- get isRecording(): boolean {
271
- return this.#isRecording;
272
- }
273
- set isRecording(value: boolean) {
274
- if (this.#isRecording !== value) {
275
- this.#isRecording = value;
276
- this.requestUpdate();
277
- }
278
- }
279
-
280
- #isToggling = false;
281
- get isToggling(): boolean {
282
- return this.#isToggling;
283
- }
284
- set isToggling(value: boolean) {
285
- if (this.#isToggling !== value) {
286
- this.#isToggling = value;
287
- this.requestUpdate();
288
- }
289
- }
290
-
291
- // TODO: we keep the functionality to allow/disallow replay but right now it's not used.
292
- // It can be used to decide if we allow replay on a certain target for example.
293
- #replayAllowed = true;
294
-
295
- #recordingPlayer?: Models.RecordingPlayer.RecordingPlayer;
296
- get recordingPlayer(): Models.RecordingPlayer.RecordingPlayer|undefined {
297
- return this.#recordingPlayer;
298
- }
299
- set recordingPlayer(value: Models.RecordingPlayer.RecordingPlayer|undefined) {
300
- if (this.#recordingPlayer !== value) {
301
- this.#recordingPlayer = value;
302
- this.requestUpdate();
303
- }
304
- }
305
-
306
- #lastReplayResult?: Models.RecordingPlayer.ReplayResult;
307
- get lastReplayResult(): Models.RecordingPlayer.ReplayResult|undefined {
308
- return this.#lastReplayResult;
309
- }
310
- set lastReplayResult(value: Models.RecordingPlayer.ReplayResult|undefined) {
311
- if (this.#lastReplayResult !== value) {
312
- this.#lastReplayResult = value;
313
- this.requestUpdate();
314
- }
315
- }
316
-
317
- readonly #replayState: ReplayState = {isPlaying: false, isPausedOnBreakpoint: false};
318
-
319
- #currentPage: Pages = Pages.START_PAGE;
320
- get currentPage(): Pages {
321
- return this.#currentPage;
322
- }
323
- set currentPage(value: Pages) {
324
- if (this.#currentPage !== value) {
325
- this.#currentPage = value;
326
- this.requestUpdate();
327
- }
328
- }
329
-
330
- #previousPage?: Pages;
331
- get previousPage(): Pages|undefined {
332
- return this.#previousPage;
333
- }
334
- set previousPage(value: Pages|undefined) {
335
- if (this.#previousPage !== value) {
336
- this.#previousPage = value;
337
- this.requestUpdate();
338
- }
339
- }
340
-
341
- #fileSelector?: HTMLInputElement;
342
-
343
- #sections?: Models.Section.Section[];
344
- get sections(): Models.Section.Section[]|undefined {
345
- return this.#sections;
346
- }
347
- set sections(value: Models.Section.Section[]|undefined) {
348
- if (this.#sections !== value) {
349
- this.#sections = value;
350
- this.requestUpdate();
351
- }
352
- }
353
-
354
- #settings?: Models.RecordingSettings.RecordingSettings;
355
- get settings(): Models.RecordingSettings.RecordingSettings|undefined {
356
- return this.#settings;
357
- }
358
- set settings(value: Models.RecordingSettings.RecordingSettings|undefined) {
359
- if (this.#settings !== value) {
360
- this.#settings = value;
361
- this.requestUpdate();
362
- }
363
- }
364
-
365
- #importError?: Error;
366
- get importError(): Error|undefined {
367
- return this.#importError;
368
- }
369
- set importError(value: Error|undefined) {
370
- if (this.#importError !== value) {
371
- this.#importError = value;
372
- this.requestUpdate();
373
- }
374
- }
375
-
376
- #exportMenuExpanded = false;
377
- get exportMenuExpanded(): boolean {
378
- return this.#exportMenuExpanded;
379
- }
380
- set exportMenuExpanded(value: boolean) {
381
- if (this.#exportMenuExpanded !== value) {
382
- this.#exportMenuExpanded = value;
383
- this.requestUpdate();
384
- }
385
- }
386
-
387
- #exportMenuButton: Buttons.Button.Button|undefined;
388
-
389
- #stepBreakpointIndexes = new Set<number>();
390
-
391
- #builtInConverters: readonly Converters.Converter.Converter[];
392
- #extensionConverters: Converters.Converter.Converter[] = [];
393
- get extensionConverters(): Converters.Converter.Converter[] {
394
- return this.#extensionConverters;
395
- }
396
- set extensionConverters(value: Converters.Converter.Converter[]) {
397
- if (this.#extensionConverters !== value) {
398
- this.#extensionConverters = value;
399
- this.requestUpdate();
400
- }
401
- }
402
-
403
- #replayExtensions: Extensions.ExtensionManager.Extension[] = [];
404
- get replayExtensions(): Extensions.ExtensionManager.Extension[] {
405
- return this.#replayExtensions;
406
- }
407
- set replayExtensions(value: Extensions.ExtensionManager.Extension[]) {
408
- if (this.#replayExtensions !== value) {
409
- this.#replayExtensions = value;
410
- this.requestUpdate();
411
- }
412
- }
413
-
414
- #viewDescriptor?: PublicExtensions.RecorderPluginManager.ViewDescriptor;
415
- get viewDescriptor(): PublicExtensions.RecorderPluginManager.ViewDescriptor|undefined {
416
- return this.#viewDescriptor;
417
- }
418
- set viewDescriptor(value: PublicExtensions.RecorderPluginManager.ViewDescriptor|undefined) {
419
- if (this.#viewDescriptor !== value) {
420
- this.#viewDescriptor = value;
421
- this.requestUpdate();
422
- }
423
- }
424
- #extensionViewShowRequestedListener?:
425
- (event: Common.EventTarget.EventTargetEvent<PublicExtensions.RecorderPluginManager.ViewDescriptor>) => void;
426
-
427
- #recorderSettings = new Models.RecorderSettings.RecorderSettings();
428
- #shortcutHelper = new Models.RecorderShortcutHelper.RecorderShortcutHelper();
429
-
430
- #disableRecorderImportWarningSetting = Common.Settings.Settings.instance().createSetting(
431
- 'disable-recorder-import-warning', false, Common.Settings.SettingStorageType.SYNCED);
432
- #selfXssWarningDisabledSetting = Common.Settings.Settings.instance().createSetting(
433
- 'disable-self-xss-warning', false, Common.Settings.SettingStorageType.SYNCED);
434
-
435
- #recordingView?: RecordingView;
436
- #createRecordingView?: CreateRecordingView;
437
-
438
- constructor() {
439
- const element = document.createElement('devtools-recorder-controller');
440
- super(element, {useShadowDom: 'pure'});
441
-
442
- this.isRecording = false;
443
- this.isToggling = false;
444
- this.exportMenuExpanded = false;
445
-
446
- this.currentPage = Pages.START_PAGE;
447
- if (this.#storage.getRecordings().length) {
448
- this.#setCurrentPage(Pages.ALL_RECORDINGS_PAGE);
449
- }
450
-
451
- const textEditorIndent = Common.Settings.Settings.instance().moduleSetting('text-editor-indent').get();
452
- this.#builtInConverters = Object.freeze([
453
- new Converters.JSONConverter.JSONConverter(textEditorIndent),
454
- new Converters.PuppeteerReplayConverter.PuppeteerReplayConverter(textEditorIndent),
455
- new Converters.PuppeteerConverter.PuppeteerConverter(textEditorIndent),
456
- new Converters.PuppeteerFirefoxConverter.PuppeteerFirefoxConverter(textEditorIndent),
457
- new Converters.LighthouseConverter.LighthouseConverter(textEditorIndent),
458
- ]);
459
-
460
- const extensionManager = Extensions.ExtensionManager.ExtensionManager.instance();
461
- this.#updateExtensions(extensionManager.extensions());
462
- extensionManager.addEventListener(Extensions.ExtensionManager.Events.EXTENSIONS_UPDATED, event => {
463
- this.#updateExtensions(event.data);
464
- });
465
-
466
- // used in e2e tests only.
467
- /* eslint-disable-next-line @devtools/no-imperative-dom-api */
468
- this.element.addEventListener('setrecording', (event: Event) => this.#onSetRecording(event));
469
- }
470
-
471
- override onDetach(): void {
472
- super.onDetach();
473
-
474
- if (this.currentRecordingSession) {
475
- void this.currentRecordingSession.stop();
476
- }
477
- if (this.#extensionViewShowRequestedListener) {
478
- PublicExtensions.RecorderPluginManager.RecorderPluginManager.instance().removeEventListener(
479
- PublicExtensions.RecorderPluginManager.Events.SHOW_VIEW_REQUESTED,
480
- this.#extensionViewShowRequestedListener,
481
- );
482
- this.#extensionViewShowRequestedListener = undefined;
483
- }
484
- }
485
-
486
- #updateExtensions(extensions: Extensions.ExtensionManager.Extension[]): void {
487
- this.extensionConverters =
488
- extensions.filter(extension => extension.getCapabilities().includes('export')).map((extension, idx) => {
489
- return new Converters.ExtensionConverter.ExtensionConverter(idx, extension);
490
- });
491
- this.replayExtensions = extensions.filter(extension => extension.getCapabilities().includes('replay'));
492
- }
493
-
494
- setIsRecordingStateForTesting(isRecording: boolean): void {
495
- this.isRecording = isRecording;
496
- }
497
-
498
- setRecordingStateForTesting(state: ReplayState): void {
499
- this.#replayState.isPlaying = state.isPlaying;
500
- this.#replayState.isPausedOnBreakpoint = state.isPausedOnBreakpoint;
501
- }
502
-
503
- setCurrentPageForTesting(page: Pages): void {
504
- this.#setCurrentPage(page);
505
- }
506
-
507
- getCurrentPageForTesting(): Pages {
508
- return this.currentPage;
509
- }
510
-
511
- getCurrentRecordingForTesting(): StoredRecording|undefined {
512
- return this.currentRecording;
513
- }
514
-
515
- getStepBreakpointIndexesForTesting(): number[] {
516
- return [...this.#stepBreakpointIndexes.values()];
517
- }
518
-
519
- /**
520
- * We should clear errors on every new action in the controller.
521
- * TODO: think how to make handle this centrally so that in no case
522
- * the error remains shown for longer than needed. Maybe a timer?
523
- */
524
- #clearError(): void {
525
- this.importError = undefined;
526
- }
527
-
528
- async #importFile(file: File): Promise<void> {
529
- const outputStream = new Common.StringOutputStream.StringOutputStream();
530
- const reader = new Bindings.FileUtils.ChunkedFileReader(file,
531
- /* chunkSize */ 10000000);
532
- const success = await reader.read(outputStream);
533
- if (!success) {
534
- throw reader.error() ?? new Error('Unknown');
535
- }
536
-
537
- let flow: Models.Schema.UserFlow|undefined;
538
- try {
539
- flow = Models.SchemaUtils.parse(JSON.parse(outputStream.data()));
540
- verifyFlowSize(flow);
541
-
542
- } catch (error) {
543
- this.importError = error;
544
- return;
545
- }
546
- this.#setCurrentRecording(await this.#storage.upsertRecording(flow));
547
- this.#setCurrentPage(Pages.RECORDING_PAGE);
548
- this.#clearError();
549
- UI.ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.recordingImported));
550
- }
551
-
552
- setCurrentRecordingForTesting(recording: StoredRecording|undefined): void {
553
- this.#setCurrentRecording(recording);
554
- }
555
-
556
- getSectionsForTesting(): Models.Section.Section[]|undefined {
557
- return this.sections;
558
- }
559
-
560
- #setCurrentRecording(recording: StoredRecording|undefined, opts: Partial<SetCurrentRecordingOptions> = {}): void {
561
- const {keepBreakpoints = false, updateSession = false} = opts;
562
- this.recordingPlayer?.abort();
563
- this.currentStep = undefined;
564
- this.recordingError = undefined;
565
- this.lastReplayResult = undefined;
566
- this.recordingPlayer = undefined;
567
- this.#replayState.isPlaying = false;
568
- this.#replayState.isPausedOnBreakpoint = false;
569
- this.#stepBreakpointIndexes = keepBreakpoints ? this.#stepBreakpointIndexes : new Set();
570
-
571
- if (recording) {
572
- this.currentRecording = recording;
573
- this.sections = Models.Section.buildSections(recording.flow.steps);
574
- this.settings = this.#buildSettings(recording.flow);
575
- if (updateSession && this.currentRecordingSession) {
576
- this.currentRecordingSession.overwriteUserFlow(recording.flow);
577
- }
578
- } else {
579
- this.currentRecording = undefined;
580
- this.sections = undefined;
581
- this.settings = undefined;
582
- }
583
-
584
- this.#updateScreenshotsForSections();
585
- }
586
-
587
- #setCurrentPage(page: Pages): void {
588
- if (page === this.currentPage) {
589
- return;
590
- }
591
-
592
- this.previousPage = this.currentPage;
593
- this.currentPage = page;
594
- }
595
-
596
- #buildSettings(flow: Models.Schema.UserFlow): Models.RecordingSettings.RecordingSettings {
597
- const steps = flow.steps;
598
- const navigateStepIdx = steps.findIndex(step => step.type === 'navigate');
599
- const settings: Models.RecordingSettings.RecordingSettings = {timeout: flow.timeout};
600
- for (let i = navigateStepIdx - 1; i >= 0; i--) {
601
- const step = steps[i];
602
- if (!settings.viewportSettings && step.type === 'setViewport') {
603
- settings.viewportSettings = step;
604
- }
605
- if (!settings.networkConditionsSettings && step.type === 'emulateNetworkConditions') {
606
- settings.networkConditionsSettings = {...step};
607
- for (const preset of [SDK.NetworkManager.OfflineConditions, SDK.NetworkManager.Slow3GConditions,
608
- SDK.NetworkManager.Slow4GConditions, SDK.NetworkManager.Fast4GConditions]) {
609
- // Using i18nTitleKey as a title here because we only want to compare the parameters of the network conditions.
610
- if (SDK.NetworkManager.networkConditionsEqual(
611
- {...preset, title: preset.i18nTitleKey || ''},
612
- // The key below is not used, but we need it to satisfy TS.
613
- {
614
- ...step,
615
- title: preset.i18nTitleKey || '',
616
- key: `step_${i}_recorder_key` as SDK.NetworkManager.UserDefinedThrottlingConditionKey
617
- })) {
618
- settings.networkConditionsSettings.title = preset.title instanceof Function ? preset.title() : preset.title;
619
- settings.networkConditionsSettings.i18nTitleKey = preset.i18nTitleKey;
620
- }
621
- }
622
- }
623
- }
624
- return settings;
625
- }
626
-
627
- #getMainTarget(): SDK.Target.Target {
628
- const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
629
- if (!target) {
630
- throw new Error('Missing main page target');
631
- }
632
- return target;
633
- }
634
-
635
- #getSectionFromStep(step: Models.Schema.Step): Models.Section.Section|null {
636
- if (!this.sections) {
637
- return null;
638
- }
639
-
640
- for (const section of this.sections) {
641
- if (section.steps.indexOf(step) !== -1) {
642
- return section;
643
- }
644
- }
645
-
646
- return null;
647
- }
648
-
649
- #updateScreenshotsForSections(): void {
650
- if (!this.sections || !this.currentRecording) {
651
- return;
652
- }
653
- const storageName = this.currentRecording.storageName;
654
- for (let i = 0; i < this.sections.length; i++) {
655
- const screenshot = this.#screenshotStorage.getScreenshotForSection(storageName, i);
656
- this.sections[i].screenshot = screenshot || undefined;
657
- }
658
- this.requestUpdate();
659
- }
660
-
661
- #onAbortReplay(): void {
662
- this.recordingPlayer?.abort();
663
- }
664
-
665
- async #onPlayViaExtension(extension: Extensions.ExtensionManager.Extension): Promise<void> {
666
- if (!this.currentRecording || !this.#replayAllowed) {
667
- return;
668
- }
669
- const pluginManager = PublicExtensions.RecorderPluginManager.RecorderPluginManager.instance();
670
-
671
- if (this.#extensionViewShowRequestedListener) {
672
- pluginManager.removeEventListener(
673
- PublicExtensions.RecorderPluginManager.Events.SHOW_VIEW_REQUESTED,
674
- this.#extensionViewShowRequestedListener,
675
- );
676
- this.#extensionViewShowRequestedListener = undefined;
677
- }
678
-
679
- let resolveView: (descriptor: PublicExtensions.RecorderPluginManager.ViewDescriptor) => void;
680
- const promise = new Promise<PublicExtensions.RecorderPluginManager.ViewDescriptor>(resolve => {
681
- resolveView = resolve;
682
- });
683
-
684
- this.#extensionViewShowRequestedListener =
685
- (event: Common.EventTarget.EventTargetEvent<PublicExtensions.RecorderPluginManager.ViewDescriptor>): void => {
686
- const descriptor = event.data;
687
- if (descriptor.extensionOrigin === extension.getOrigin()) {
688
- if (this.#extensionViewShowRequestedListener) {
689
- pluginManager.removeEventListener(
690
- PublicExtensions.RecorderPluginManager.Events.SHOW_VIEW_REQUESTED,
691
- this.#extensionViewShowRequestedListener,
692
- );
693
- this.#extensionViewShowRequestedListener = undefined;
694
- }
695
- resolveView(descriptor);
696
- }
697
- };
698
-
699
- pluginManager.addEventListener(
700
- PublicExtensions.RecorderPluginManager.Events.SHOW_VIEW_REQUESTED,
701
- this.#extensionViewShowRequestedListener,
702
- );
703
-
704
- extension.replay(this.currentRecording.flow);
705
- const descriptor = await promise;
706
- this.viewDescriptor = descriptor;
707
- Host.userMetrics.recordingReplayStarted(Host.UserMetrics.RecordingReplayStarted.REPLAY_VIA_EXTENSION);
708
- }
709
-
710
- async #onPlayRecording(event: ViewPlayRecordingEvent): Promise<void> {
711
- if (!this.currentRecording || !this.#replayAllowed) {
712
- return;
713
- }
714
- if (this.viewDescriptor) {
715
- this.viewDescriptor = undefined;
716
- }
717
- if (this.#extensionViewShowRequestedListener) {
718
- PublicExtensions.RecorderPluginManager.RecorderPluginManager.instance().removeEventListener(
719
- PublicExtensions.RecorderPluginManager.Events.SHOW_VIEW_REQUESTED,
720
- this.#extensionViewShowRequestedListener,
721
- );
722
- this.#extensionViewShowRequestedListener = undefined;
723
- }
724
- if (event.extension) {
725
- return await this.#onPlayViaExtension(event.extension);
726
- }
727
- Host.userMetrics.recordingReplayStarted(
728
- event.targetPanel !== TargetPanel.DEFAULT ?
729
- Host.UserMetrics.RecordingReplayStarted.REPLAY_WITH_PERFORMANCE_TRACING :
730
- Host.UserMetrics.RecordingReplayStarted.REPLAY_ONLY);
731
- this.#replayState.isPlaying = true;
732
- this.currentStep = undefined;
733
- this.recordingError = undefined;
734
- this.lastReplayResult = undefined;
735
- const currentRecording = this.currentRecording;
736
- this.#clearError();
737
-
738
- await this.#disableDeviceModeIfEnabled();
739
-
740
- this.recordingPlayer = new Models.RecordingPlayer.RecordingPlayer(
741
- this.currentRecording.flow, {speed: event.speed, breakpointIndexes: this.#stepBreakpointIndexes});
742
-
743
- const withPerformanceTrace = event.targetPanel === TargetPanel.PERFORMANCE_PANEL;
744
- const sectionsWithScreenshot = new Set();
745
- this.recordingPlayer.addEventListener(Models.RecordingPlayer.Events.STEP, async ({data: {step, resolve}}) => {
746
- this.currentStep = step;
747
- const currentSection = this.#getSectionFromStep(step);
748
- if (this.sections && currentSection && !sectionsWithScreenshot.has(currentSection)) {
749
- sectionsWithScreenshot.add(currentSection);
750
- const currentSectionIndex = this.sections.indexOf(currentSection);
751
- const screenshot = await Models.ScreenshotUtils.takeScreenshot();
752
- currentSection.screenshot = screenshot;
753
- Models.ScreenshotStorage.ScreenshotStorage.instance().storeScreenshotForSection(
754
- currentRecording.storageName, currentSectionIndex, screenshot);
755
- }
756
- resolve();
757
- });
758
-
759
- this.recordingPlayer.addEventListener(Models.RecordingPlayer.Events.STOP, () => {
760
- this.#replayState.isPausedOnBreakpoint = true;
761
- this.requestUpdate();
762
- });
763
-
764
- this.recordingPlayer.addEventListener(Models.RecordingPlayer.Events.CONTINUE, () => {
765
- this.#replayState.isPausedOnBreakpoint = false;
766
- this.requestUpdate();
767
- });
768
-
769
- this.recordingPlayer.addEventListener(Models.RecordingPlayer.Events.ERROR, ({data: error}) => {
770
- this.recordingError = error;
771
- if (!withPerformanceTrace) {
772
- this.#replayState.isPlaying = false;
773
- this.recordingPlayer = undefined;
774
- }
775
- this.lastReplayResult = Models.RecordingPlayer.ReplayResult.FAILURE;
776
- const errorMessage = error.message.toLowerCase();
777
- if (errorMessage.startsWith('could not find element')) {
778
- Host.userMetrics.recordingReplayFinished(Host.UserMetrics.RecordingReplayFinished.TIMEOUT_ERROR_SELECTORS);
779
- } else if (errorMessage.startsWith('waiting for target failed')) {
780
- Host.userMetrics.recordingReplayFinished(Host.UserMetrics.RecordingReplayFinished.TIMEOUT_ERROR_TARGET);
781
- } else {
782
- Host.userMetrics.recordingReplayFinished(Host.UserMetrics.RecordingReplayFinished.OTHER_ERROR);
783
- }
784
- // Dispatch an event for e2e testing.
785
- this.element.dispatchEvent(new Events.ReplayFinishedEvent());
786
- });
787
-
788
- this.recordingPlayer.addEventListener(Models.RecordingPlayer.Events.DONE, () => {
789
- if (!withPerformanceTrace) {
790
- this.#replayState.isPlaying = false;
791
- this.recordingPlayer = undefined;
792
- }
793
- this.lastReplayResult = Models.RecordingPlayer.ReplayResult.SUCCESS;
794
- // Dispatch an event for e2e testing.
795
- this.element.dispatchEvent(new Events.ReplayFinishedEvent());
796
- Host.userMetrics.recordingReplayFinished(Host.UserMetrics.RecordingReplayFinished.SUCCESS);
797
- });
798
-
799
- this.recordingPlayer.addEventListener(Models.RecordingPlayer.Events.ABORT, () => {
800
- this.currentStep = undefined;
801
- this.recordingError = undefined;
802
- this.lastReplayResult = undefined;
803
- this.#replayState.isPlaying = false;
804
- });
805
-
806
- let resolveWithEvents = (_events: Object[]): void => {};
807
- const eventsPromise = new Promise<Object[]>((resolve): void => {
808
- resolveWithEvents = resolve;
809
- });
810
-
811
- let performanceTracing = null;
812
- switch (event.targetPanel) {
813
- case TargetPanel.PERFORMANCE_PANEL:
814
- performanceTracing = new Tracing.PerformanceTracing.PerformanceTracing(this.#getMainTarget(), {
815
- tracingBufferUsage(): void{},
816
- eventsRetrievalProgress(): void{},
817
- tracingComplete(events: Object[]): void {
818
- resolveWithEvents(events);
819
- },
820
- });
821
- break;
822
- }
823
-
824
- if (performanceTracing) {
825
- await performanceTracing.start();
826
- }
827
-
828
- this.#setTouchEmulationAllowed(false);
829
- await this.recordingPlayer.play();
830
- this.#setTouchEmulationAllowed(true);
831
-
832
- if (performanceTracing) {
833
- await performanceTracing.stop();
834
- const events = await eventsPromise;
835
- this.#replayState.isPlaying = false;
836
- this.recordingPlayer = undefined;
837
- await UI.InspectorView.InspectorView.instance().showPanel(event.targetPanel as string);
838
- if (event.targetPanel === TargetPanel.PERFORMANCE_PANEL) {
839
- // Note: this is not passing any metadata to the Performance panel.
840
- const trace = new SDK.TraceObject.TraceObject(events as Trace.Types.Events.Event[]);
841
- void Common.Revealer.reveal(trace);
842
- }
843
- }
844
- }
845
-
846
- async #disableDeviceModeIfEnabled(): Promise<void> {
847
- try {
848
- const deviceModeWrapper = Emulation.DeviceModeWrapper.DeviceModeWrapper.instance();
849
- if (deviceModeWrapper.isDeviceModeOn()) {
850
- deviceModeWrapper.toggleDeviceMode();
851
- const emulationModel = this.#getMainTarget().model(SDK.EmulationModel.EmulationModel);
852
- await emulationModel?.emulateDevice(null);
853
- }
854
- } catch {
855
- // in the hosted mode, when the DeviceMode toolbar is not supported,
856
- // Emulation.DeviceModeWrapper.DeviceModeWrapper.instance throws an exception.
857
- }
858
- }
859
-
860
- #setTouchEmulationAllowed(touchEmulationAllowed: boolean): void {
861
- const emulationModel = this.#getMainTarget().model(SDK.EmulationModel.EmulationModel);
862
- emulationModel?.setTouchEmulationAllowed(touchEmulationAllowed);
863
- }
864
-
865
- async #onSetRecording(event: Event): Promise<void> {
866
- const json = JSON.parse((event as CustomEvent).detail);
867
- this.#setCurrentRecording(await this.#storage.upsertRecording(Models.SchemaUtils.parse(json)));
868
- this.#setCurrentPage(Pages.RECORDING_PAGE);
869
- this.#clearError();
870
- this.element.dispatchEvent(new Events.SetRecordingFinishedEvent());
871
- }
872
-
873
- // Used by e2e tests to inspect the current recording.
874
- getUserFlow(): Models.Schema.UserFlow|undefined {
875
- return this.currentRecording?.flow;
876
- }
877
-
878
- async #handleRecordingChanged(event: StepChanged): Promise<void> {
879
- if (!this.currentRecording) {
880
- throw new Error('Current recording expected to be defined.');
881
- }
882
- const recording = {
883
- ...this.currentRecording,
884
- flow: {
885
- ...this.currentRecording.flow,
886
- steps: this.currentRecording.flow.steps.map(step => step === event.currentStep ? event.newStep : step),
887
- },
888
- };
889
- this.#setCurrentRecording(await this.#storage.upsertRecording(
890
- recording.flow,
891
- recording.storageName,
892
- ),
893
- {keepBreakpoints: true, updateSession: true});
894
- }
895
-
896
- async #handleStepAdded(event: AddStep): Promise<void> {
897
- if (!this.currentRecording) {
898
- throw new Error('Current recording expected to be defined.');
899
- }
900
- const stepOrSection = event.stepOrSection;
901
- let step;
902
- let position = event.position;
903
- if ('steps' in stepOrSection) {
904
- // section
905
- const sectionIdx = this.sections?.indexOf(stepOrSection);
906
- if (sectionIdx === undefined || sectionIdx === -1) {
907
- throw new Error('There is no section to add a step to');
908
- }
909
- if (event.position === AddStepPosition.AFTER) {
910
- if (this.sections?.[sectionIdx].steps.length) {
911
- step = this.sections?.[sectionIdx].steps[0];
912
- position = AddStepPosition.BEFORE;
913
- } else {
914
- step = this.sections?.[sectionIdx].causingStep;
915
- position = AddStepPosition.AFTER;
916
- }
917
- } else {
918
- if (sectionIdx <= 0) {
919
- throw new Error('There is no section to add a step to');
920
- }
921
- const prevSection = this.sections?.[sectionIdx - 1];
922
- step = prevSection?.steps[prevSection.steps.length - 1];
923
- position = AddStepPosition.AFTER;
924
- }
925
- } else {
926
- // step
927
- step = stepOrSection;
928
- }
929
- if (!step) {
930
- throw new Error('Anchor step is not found when adding a step');
931
- }
932
- const steps = this.currentRecording.flow.steps;
933
- const currentIndex = steps.indexOf(step);
934
- const indexToInsertAt = currentIndex + (position === AddStepPosition.BEFORE ? 0 : 1);
935
- steps.splice(indexToInsertAt, 0, {type: Models.Schema.StepType.WaitForElement, selectors: ['body']});
936
- const recording = {...this.currentRecording, flow: {...this.currentRecording.flow, steps}};
937
- this.#stepBreakpointIndexes = new Set([...this.#stepBreakpointIndexes.values()].map(breakpointIndex => {
938
- if (indexToInsertAt > breakpointIndex) {
939
- return breakpointIndex;
940
- }
941
-
942
- return breakpointIndex + 1;
943
- }));
944
- this.#setCurrentRecording(await this.#storage.upsertRecording(
945
- recording.flow,
946
- recording.storageName,
947
- ),
948
- {keepBreakpoints: true, updateSession: true});
949
- }
950
-
951
- async #handleRecordingTitleChanged(title: string): Promise<void> {
952
- if (!this.currentRecording) {
953
- throw new Error('Current recording expected to be defined.');
954
- }
955
-
956
- const flow = {...this.currentRecording.flow, title};
957
- this.#setCurrentRecording(await this.#storage.upsertRecording(
958
- flow,
959
- this.currentRecording.storageName,
960
- ));
961
- }
962
-
963
- async #handleStepRemoved(event: RemoveStep): Promise<void> {
964
- if (!this.currentRecording) {
965
- throw new Error('Current recording expected to be defined.');
966
- }
967
-
968
- const steps = this.currentRecording.flow.steps;
969
- const currentIndex = steps.indexOf(event.step);
970
- steps.splice(currentIndex, 1);
971
- const flow = {...this.currentRecording.flow, steps};
972
- this.#stepBreakpointIndexes = new Set([...this.#stepBreakpointIndexes.values()]
973
- .map(breakpointIndex => {
974
- if (currentIndex > breakpointIndex) {
975
- return breakpointIndex;
976
- }
977
-
978
- if (currentIndex === breakpointIndex) {
979
- return -1;
980
- }
981
-
982
- return breakpointIndex - 1;
983
- })
984
- .filter(index => index >= 0));
985
- this.#setCurrentRecording(await this.#storage.upsertRecording(
986
- flow,
987
- this.currentRecording.storageName,
988
- ),
989
- {keepBreakpoints: true, updateSession: true});
990
- }
991
-
992
- async #onNetworkConditionsChanged(data?: SDK.NetworkManager.Conditions): Promise<void> {
993
- if (!this.currentRecording) {
994
- throw new Error('Current recording expected to be defined.');
995
- }
996
- const navigateIdx = this.currentRecording.flow.steps.findIndex(step => step.type === 'navigate');
997
- if (navigateIdx === -1) {
998
- throw new Error('Current recording does not have a navigate step');
999
- }
1000
- const emulateNetworkConditionsIdx = this.currentRecording.flow.steps.findIndex((step, idx) => {
1001
- if (idx >= navigateIdx) {
1002
- return false;
1003
- }
1004
- return step.type === 'emulateNetworkConditions';
1005
- });
1006
- if (!data) {
1007
- // Delete step if present.
1008
- if (emulateNetworkConditionsIdx !== -1) {
1009
- this.currentRecording.flow.steps.splice(emulateNetworkConditionsIdx, 1);
1010
- }
1011
- } else if (emulateNetworkConditionsIdx === -1) {
1012
- // Insert at the first position.
1013
- this.currentRecording.flow.steps.splice(
1014
- 0, 0,
1015
- Models.SchemaUtils.createEmulateNetworkConditionsStep(
1016
- {download: data.download, upload: data.upload, latency: data.latency}));
1017
- } else {
1018
- // Update existing step.
1019
- const step =
1020
- this.currentRecording.flow.steps[emulateNetworkConditionsIdx] as Models.Schema.EmulateNetworkConditionsStep;
1021
- step.download = data.download;
1022
- step.upload = data.upload;
1023
- step.latency = data.latency;
1024
- }
1025
- this.#setCurrentRecording(await this.#storage.upsertRecording(
1026
- this.currentRecording.flow,
1027
- this.currentRecording.storageName,
1028
- ));
1029
- }
1030
-
1031
- async #onTimeoutChanged(timeout?: number): Promise<void> {
1032
- if (!this.currentRecording) {
1033
- throw new Error('Current recording expected to be defined.');
1034
- }
1035
- this.currentRecording.flow.timeout = timeout;
1036
- this.#setCurrentRecording(await this.#storage.upsertRecording(
1037
- this.currentRecording.flow,
1038
- this.currentRecording.storageName,
1039
- ));
1040
- }
1041
-
1042
- async #onDeleteRecording(event: Event): Promise<void> {
1043
- event.stopPropagation();
1044
- if (event instanceof DeleteRecordingEvent) {
1045
- await this.#storage.deleteRecording(event.storageName);
1046
- this.#screenshotStorage.deleteScreenshotsForRecording(event.storageName);
1047
- this.requestUpdate();
1048
- } else {
1049
- if (!this.currentRecording) {
1050
- return;
1051
- }
1052
- await this.#storage.deleteRecording(this.currentRecording.storageName);
1053
- this.#screenshotStorage.deleteScreenshotsForRecording(this.currentRecording.storageName);
1054
- }
1055
- UI.ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.recordingDeleted));
1056
- if ((await this.#storage.getRecordings()).length) {
1057
- this.#setCurrentPage(Pages.ALL_RECORDINGS_PAGE);
1058
- } else {
1059
- this.#setCurrentPage(Pages.START_PAGE);
1060
- }
1061
- this.#setCurrentRecording(undefined);
1062
- this.#clearError();
1063
- }
1064
-
1065
- #onCreateNewRecording(event?: Event): void {
1066
- event?.stopPropagation();
1067
- this.#setCurrentPage(Pages.CREATE_RECORDING_PAGE);
1068
- this.#clearError();
1069
- }
1070
-
1071
- async #onRecordingStarted(
1072
- data: {name: string, selectorTypesToRecord: Models.Schema.SelectorType[], selectorAttribute?: string}):
1073
- Promise<void> {
1074
- // Recording is not available in device mode.
1075
- await this.#disableDeviceModeIfEnabled();
1076
-
1077
- // Setting up some variables to notify the user we are initializing a recording.
1078
- this.isToggling = true;
1079
- this.#clearError();
1080
-
1081
- // -- Recording logic starts here --
1082
- Host.userMetrics.recordingToggled(Host.UserMetrics.RecordingToggled.RECORDING_STARTED);
1083
- this.currentRecordingSession = new Models.RecordingSession.RecordingSession(this.#getMainTarget(), {
1084
- title: data.name,
1085
- selectorAttribute: data.selectorAttribute,
1086
- selectorTypesToRecord: data.selectorTypesToRecord.length ? data.selectorTypesToRecord :
1087
- Object.values(Models.Schema.SelectorType),
1088
- });
1089
- this.#setCurrentRecording(await this.#storage.upsertRecording(this.currentRecordingSession.cloneUserFlow()));
1090
-
1091
- let previousSectionIndex = -1;
1092
- let screenshotPromise:|Promise<Models.ScreenshotStorage.Screenshot>|undefined;
1093
- const takeScreenshot = async(currentRecording: StoredRecording): Promise<void> => {
1094
- if (!this.sections) {
1095
- throw new Error('Could not find sections.');
1096
- }
1097
-
1098
- const currentSectionIndex = this.sections.length - 1;
1099
- const currentSection = this.sections[currentSectionIndex];
1100
- if (screenshotPromise || previousSectionIndex === currentSectionIndex) {
1101
- return;
1102
- }
1103
-
1104
- screenshotPromise = Models.ScreenshotUtils.takeScreenshot();
1105
- const screenshot = await screenshotPromise;
1106
- screenshotPromise = undefined;
1107
- currentSection.screenshot = screenshot;
1108
- Models.ScreenshotStorage.ScreenshotStorage.instance().storeScreenshotForSection(currentRecording.storageName,
1109
- currentSectionIndex, screenshot);
1110
- previousSectionIndex = currentSectionIndex;
1111
- this.#updateScreenshotsForSections();
1112
- };
1113
-
1114
- this.currentRecordingSession.addEventListener(Models.RecordingSession.Events.RECORDING_UPDATED,
1115
- async ({data}: {data: Models.Schema.UserFlow}) => {
1116
- if (!this.currentRecording) {
1117
- throw new Error('No current recording found');
1118
- }
1119
- this.#setCurrentRecording(await this.#storage.upsertRecording(
1120
- data,
1121
- this.currentRecording.storageName,
1122
- ));
1123
- this.#recordingView?.scrollToBottom();
1124
-
1125
- await takeScreenshot(this.currentRecording);
1126
- });
1127
-
1128
- this.currentRecordingSession.addEventListener(
1129
- Models.RecordingSession.Events.RECORDING_STOPPED, async ({data}: {data: Models.Schema.UserFlow}) => {
1130
- if (!this.currentRecording) {
1131
- throw new Error('No current recording found');
1132
- }
1133
- Host.userMetrics.keyboardShortcutFired(Actions.RecorderActions.START_RECORDING);
1134
- this.#setCurrentRecording(await this.#storage.upsertRecording(
1135
- data,
1136
- this.currentRecording.storageName,
1137
- ));
1138
- await this.#onRecordingFinished();
1139
- });
1140
-
1141
- await this.currentRecordingSession.start();
1142
- // -- Recording logic ends here --
1143
-
1144
- // Setting up some variables to notify the user we are finished initialization.
1145
- this.isToggling = false;
1146
- this.isRecording = true;
1147
- this.#setCurrentPage(Pages.RECORDING_PAGE);
1148
-
1149
- // Dispatch an event for e2e testing.
1150
- this.element.dispatchEvent(new Events.RecordingStateChangedEvent((this.currentRecording as StoredRecording).flow));
1151
- }
1152
-
1153
- async #onRecordingFinished(): Promise<void> {
1154
- if (!this.currentRecording || !this.currentRecordingSession) {
1155
- throw new Error('Recording was never started');
1156
- }
1157
-
1158
- // Setting up some variables to notify the user we are finalizing a recording.
1159
- this.isToggling = true;
1160
- this.#clearError();
1161
-
1162
- // -- Recording logic starts here --
1163
- Host.userMetrics.recordingToggled(Host.UserMetrics.RecordingToggled.RECORDING_FINISHED);
1164
- await this.currentRecordingSession.stop();
1165
- this.currentRecordingSession = undefined;
1166
- // -- Recording logic ends here --
1167
-
1168
- // Setting up some variables to notify the user we are finished finalizing.
1169
- this.isToggling = false;
1170
- this.isRecording = false;
1171
-
1172
- // Dispatch an event for e2e testing.
1173
- this.element.dispatchEvent(new Events.RecordingStateChangedEvent(this.currentRecording.flow));
1174
- }
1175
-
1176
- async onRecordingCancelled(): Promise<void> {
1177
- if (this.previousPage) {
1178
- this.#setCurrentPage(this.previousPage);
1179
- }
1180
- }
1181
-
1182
- async #onRecordingSelected(event: Event): Promise<void> {
1183
- const storageName = event instanceof OpenRecordingEvent || event instanceof ListViewPlayRecordingEvent ?
1184
- event.storageName :
1185
- ((event as InputEvent).target as HTMLSelectElement)?.value;
1186
- this.#setCurrentRecording(await this.#storage.getRecording(storageName));
1187
- if (this.currentRecording) {
1188
- this.#setCurrentPage(Pages.RECORDING_PAGE);
1189
- } else if (storageName === Pages.START_PAGE) {
1190
- this.#setCurrentPage(Pages.START_PAGE);
1191
- } else if (storageName === Pages.ALL_RECORDINGS_PAGE) {
1192
- this.#setCurrentPage(Pages.ALL_RECORDINGS_PAGE);
1193
- }
1194
- }
1195
-
1196
- async #onExportOptionSelected(event: Menus.SelectMenu.SelectMenuItemSelectedEvent): Promise<void> {
1197
- if (typeof event.itemValue !== 'string') {
1198
- throw new Error('Invalid export option value');
1199
- }
1200
- if (event.itemValue === GET_EXTENSIONS_MENU_ITEM) {
1201
- Host.InspectorFrontendHost.InspectorFrontendHostInstance.openInNewTab(GET_EXTENSIONS_URL);
1202
- return;
1203
- }
1204
- if (!this.currentRecording) {
1205
- throw new Error('No recording selected');
1206
- }
1207
- const id = event.itemValue;
1208
- const byId = (converter: Converters.Converter.Converter): boolean => converter.getId() === id;
1209
- const converter = this.#builtInConverters.find(byId) || this.extensionConverters.find(byId);
1210
- if (!converter) {
1211
- throw new Error('No recording selected');
1212
- }
1213
- const [content] = await converter.stringify(this.currentRecording.flow);
1214
- await this.#exportContent(converter.getFilename(this.currentRecording.flow), content);
1215
- const builtInMetric = CONVERTER_ID_TO_METRIC[converter.getId()];
1216
- if (builtInMetric) {
1217
- UI.ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.recordingExported));
1218
- } else if (converter.getId().startsWith(Converters.ExtensionConverter.EXTENSION_PREFIX)) {
1219
- UI.ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.recordingExported));
1220
- } else {
1221
- throw new Error('Could not find a metric for the export option with id = ' + id);
1222
- }
1223
- }
1224
-
1225
- async #exportContent(suggestedName: string, data: string): Promise<void> {
1226
- try {
1227
- const handle = await window.showSaveFilePicker({suggestedName});
1228
- const writable = await handle.createWritable();
1229
- await writable.write(data);
1230
- await writable.close();
1231
- } catch (error) {
1232
- // If the user aborts the action no need to report it, otherwise do.
1233
- if (error.name === 'AbortError') {
1234
- return;
1235
- }
1236
-
1237
- throw error;
1238
- }
1239
- }
1240
-
1241
- async #handleAddAssertionEvent(): Promise<void> {
1242
- if (!this.currentRecordingSession || !this.currentRecording) {
1243
- return;
1244
- }
1245
- const flow = this.currentRecordingSession.cloneUserFlow();
1246
- flow.steps.push({type: 'waitForElement' as Models.Schema.StepType.WaitForElement, selectors: [['.cls']]});
1247
- this.#setCurrentRecording(await this.#storage.upsertRecording(
1248
- flow,
1249
- this.currentRecording.storageName,
1250
- ),
1251
- {keepBreakpoints: true, updateSession: true});
1252
- await this.updateComplete;
1253
- // FIXME: call a method on the recording view widget.
1254
- await this.#recordingView?.updateComplete;
1255
- this.#recordingView?.contentElement?.querySelector('.section:last-child .step-view-widget:last-of-type')
1256
- ?.shadowRoot?.querySelector<HTMLElement>('.action')
1257
- ?.click();
1258
- }
1259
-
1260
- async #acknowledgeImportNotice(): Promise<boolean> {
1261
- if (this.#disableRecorderImportWarningSetting.get()) {
1262
- return true;
1263
- }
1264
-
1265
- if (Root.Runtime.Runtime.queryParam('isChromeForTesting') ||
1266
- Root.Runtime.Runtime.queryParam('disableSelfXssWarnings') || this.#selfXssWarningDisabledSetting.get()) {
1267
- return true;
1268
- }
1269
-
1270
- const result = await PanelCommon.TypeToAllowDialog.show({
1271
- jslogContext: {
1272
- input: 'confirm-import-recording-input',
1273
- dialog: 'confirm-import-recording-dialog',
1274
- },
1275
- message: i18nString(UIStrings.doNotImport, {PH1: i18nString(UIStrings.allowImporting)}),
1276
- header: i18nString(UIStrings.doYouTrustThisCode),
1277
- typePhrase: i18nString(UIStrings.allowImporting),
1278
- inputPlaceholder: i18nString(UIStrings.typeAllowImporting, {PH1: i18nString(UIStrings.allowImporting)}),
1279
- });
1280
-
1281
- if (result) {
1282
- this.#disableRecorderImportWarningSetting.set(true);
1283
- }
1284
-
1285
- return result;
1286
- }
1287
-
1288
- async #onImportRecording(event: Event): Promise<void> {
1289
- event.stopPropagation();
1290
-
1291
- this.#clearError();
1292
-
1293
- if (await this.#acknowledgeImportNotice()) {
1294
- this.#fileSelector = UI.UIUtils.createFileSelectorElement(this.#importFile.bind(this));
1295
- this.#fileSelector.click();
1296
- }
1297
- }
1298
-
1299
- async #onPlayRecordingByName(event: ListViewPlayRecordingEvent): Promise<void> {
1300
- await this.#onRecordingSelected(event);
1301
- await this.#onPlayRecording({targetPanel: TargetPanel.DEFAULT, speed: this.#recorderSettings.speed});
1302
- }
1303
-
1304
- #onAddBreakpoint = (event: AddBreakpointEvent): void => {
1305
- this.#stepBreakpointIndexes = structuredClone(this.#stepBreakpointIndexes);
1306
- this.#stepBreakpointIndexes.add(event.index);
1307
- this.recordingPlayer?.updateBreakpointIndexes(this.#stepBreakpointIndexes);
1308
- this.requestUpdate();
1309
- };
1310
-
1311
- #onRemoveBreakpoint = (event: RemoveBreakpointEvent): void => {
1312
- this.#stepBreakpointIndexes = structuredClone(this.#stepBreakpointIndexes);
1313
- this.#stepBreakpointIndexes.delete(event.index);
1314
- this.recordingPlayer?.updateBreakpointIndexes(this.#stepBreakpointIndexes);
1315
- this.requestUpdate();
1316
- };
1317
-
1318
- #onExtensionViewClosed(): void {
1319
- this.viewDescriptor = undefined;
1320
- }
1321
-
1322
- handleActions(actionId: Actions.RecorderActions): void {
1323
- if (!this.isActionPossible(actionId)) {
1324
- return;
1325
- }
1326
-
1327
- switch (actionId) {
1328
- case Actions.RecorderActions.CREATE_RECORDING:
1329
- this.#onCreateNewRecording();
1330
- return;
1331
-
1332
- case Actions.RecorderActions.START_RECORDING:
1333
- if (this.currentPage !== Pages.CREATE_RECORDING_PAGE && !this.isRecording) {
1334
- this.#shortcutHelper.handleShortcut(this.#onRecordingStarted.bind(this, {
1335
- name: this.#recorderSettings.defaultTitle,
1336
- selectorTypesToRecord: this.#recorderSettings.defaultSelectors,
1337
- selectorAttribute: this.#recorderSettings.selectorAttribute ? this.#recorderSettings.selectorAttribute :
1338
- undefined,
1339
- }));
1340
- } else if (this.currentPage === Pages.CREATE_RECORDING_PAGE) {
1341
- if (this.#createRecordingView) {
1342
- this.#shortcutHelper.handleShortcut(() => {
1343
- this.#createRecordingView?.startRecording();
1344
- });
1345
- }
1346
- } else if (this.isRecording) {
1347
- void this.#onRecordingFinished();
1348
- }
1349
- return;
1350
-
1351
- case Actions.RecorderActions.REPLAY_RECORDING:
1352
- void this.#onPlayRecording({targetPanel: TargetPanel.DEFAULT, speed: this.#recorderSettings.speed});
1353
- return;
1354
-
1355
- case Actions.RecorderActions.TOGGLE_CODE_VIEW: {
1356
- this.#recordingView?.showCodeToggle();
1357
- return;
1358
- }
1359
- }
1360
- }
1361
-
1362
- isActionPossible(actionId: Actions.RecorderActions): boolean {
1363
- switch (actionId) {
1364
- case Actions.RecorderActions.CREATE_RECORDING:
1365
- return !this.isRecording && !this.#replayState.isPlaying;
1366
- case Actions.RecorderActions.START_RECORDING:
1367
- return !this.#replayState.isPlaying;
1368
- case Actions.RecorderActions.REPLAY_RECORDING:
1369
- return (this.currentPage === Pages.RECORDING_PAGE && !this.#replayState.isPlaying);
1370
- case Actions.RecorderActions.TOGGLE_CODE_VIEW:
1371
- return this.currentPage === Pages.RECORDING_PAGE;
1372
- case Actions.RecorderActions.COPY_RECORDING_OR_STEP:
1373
- // This action is handled in the RecordingView
1374
- // It relies on browser `copy` event.
1375
- return false;
1376
- }
1377
- }
1378
-
1379
- #getShortcutsInfo(): Dialogs.ShortcutDialog.Shortcut[] {
1380
- const getBindingForAction = (action: Actions.RecorderActions): Dialogs.ShortcutDialog.ShortcutPart[][] => {
1381
- const shortcuts = UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutsForAction(action);
1382
- const shortcutsWithSplitBindings = shortcuts.map(shortcut => shortcut.title().split(/[\s+]+/).map(word => {
1383
- return {key: word.trim()};
1384
- }));
1385
- return shortcutsWithSplitBindings;
1386
- };
1387
-
1388
- return [
1389
- {
1390
- title: i18nString(UIStrings.startStopRecording),
1391
- rows: getBindingForAction(Actions.RecorderActions.START_RECORDING),
1392
- },
1393
- {
1394
- title: i18nString(UIStrings.replayRecording),
1395
- rows: getBindingForAction(Actions.RecorderActions.REPLAY_RECORDING),
1396
- },
1397
- {
1398
- title: i18nString(UIStrings.copyShortcut),
1399
- rows: Host.Platform.isMac() ? [[{key: '⌘'}, {key: 'C'}]] : [[{key: 'Ctrl'}, {key: 'C'}]]
1400
- },
1401
- {
1402
- title: i18nString(UIStrings.toggleCode),
1403
- rows: getBindingForAction(Actions.RecorderActions.TOGGLE_CODE_VIEW),
1404
- },
1405
- ];
1406
- }
1407
-
1408
- #renderCurrentPage(): Lit.TemplateResult {
1409
- switch (this.currentPage) {
1410
- case Pages.START_PAGE:
1411
- return this.#renderStartPage();
1412
- case Pages.ALL_RECORDINGS_PAGE:
1413
- return this.#renderAllRecordingsPage();
1414
- case Pages.RECORDING_PAGE:
1415
- return this.#renderRecordingPage();
1416
- case Pages.CREATE_RECORDING_PAGE:
1417
- return this.#renderCreateRecordingPage();
1418
- }
1419
- }
1420
-
1421
- #renderAllRecordingsPage(): Lit.TemplateResult {
1422
- const recordings = this.#storage.getRecordings();
1423
- // clang-format off
1424
- return html`
1425
- <devtools-widget
1426
- ${widget(RecordingListView, {
1427
- recordings: recordings.map(recording => ({
1428
- storageName: recording.storageName,
1429
- name: recording.flow.title,
1430
- })),
1431
- replayAllowed: this.#replayAllowed,
1432
- })}
1433
- @createrecording=${this.#onCreateNewRecording.bind(this)}
1434
- @deleterecording=${this.#onDeleteRecording.bind(this)}
1435
- @openrecording=${this.#onRecordingSelected.bind(this)}
1436
- @playrecording=${this.#onPlayRecordingByName.bind(this)}
1437
- >
1438
- </devtools-widget>
1439
- `;
1440
- // clang-format on
1441
- }
1442
-
1443
- #renderStartPage(): Lit.TemplateResult {
1444
- // clang-format off
1445
- return html`
1446
- <div class="empty-state" jslog=${VisualLogging.section().context('start-view')}>
1447
- <div class="empty-state-header">${i18nString(UIStrings.header)}</div>
1448
- <div class="empty-state-description">
1449
- <span>${i18nString(UIStrings.recordingDescription)}</span>
1450
- <devtools-link
1451
- class="devtools-link"
1452
- href=${RECORDER_EXPLANATION_URL}
1453
- jslogcontext="learn-more"
1454
- >${i18nString(UIStrings.learnMore)}</devtools-link>
1455
- </div>
1456
- <devtools-button .variant=${Buttons.Button.Variant.TONAL} jslogContext=${Actions.RecorderActions.CREATE_RECORDING} @click=${this.#onCreateNewRecording.bind(this)}>${i18nString(UIStrings.createRecording)}</devtools-button>
1457
- </div>
1458
- `;
1459
- // clang-format on
1460
- }
1461
-
1462
- #renderRecordingPage(): Lit.TemplateResult {
1463
- // clang-format off
1464
- return html`
1465
- <devtools-widget
1466
- class="recording-view"
1467
- ${widget(RecordingView, {
1468
- recording: this.currentRecording?.flow ?? {title: '', steps: []},
1469
- replayState: this.#replayState,
1470
- isRecording: this.isRecording,
1471
- recordingTogglingInProgress: this.isToggling,
1472
- currentStep: this.currentStep,
1473
- currentError: this.recordingError,
1474
- sections: this.sections ?? [],
1475
- settings: this.settings,
1476
- recorderSettings: this.#recorderSettings,
1477
- lastReplayResult: this.lastReplayResult,
1478
- replayAllowed: this.#replayAllowed,
1479
- breakpointIndexes: this.#stepBreakpointIndexes,
1480
- builtInConverters: this.#builtInConverters,
1481
- extensionConverters: this.extensionConverters,
1482
- replayExtensions: this.replayExtensions,
1483
- extensionDescriptor: this.viewDescriptor,
1484
- recordingFinished: this.#onRecordingFinished.bind(this),
1485
- addAssertion: this.#handleAddAssertionEvent.bind(this),
1486
- abortReplay: this.#onAbortReplay.bind(this),
1487
- playRecording: this.#onPlayRecording.bind(this),
1488
- networkConditionsChanged: this.#onNetworkConditionsChanged.bind(this),
1489
- timeoutChanged: this.#onTimeoutChanged.bind(this),
1490
- titleChanged: this.#handleRecordingTitleChanged.bind(this),
1491
- })}
1492
- @requestselectorattribute=${(
1493
- event: RequestSelectorAttributeEvent,
1494
- ) => {
1495
- event.send(this.currentRecording?.flow.selectorAttribute);
1496
- }}
1497
- @stepchanged=${this.#handleRecordingChanged.bind(this)}
1498
- @addstep=${this.#handleStepAdded.bind(this)}
1499
- @removestep=${this.#handleStepRemoved.bind(this)}
1500
- @addbreakpoint=${this.#onAddBreakpoint.bind(this)}
1501
- @removebreakpoint=${this.#onRemoveBreakpoint.bind(this)}
1502
- @recorderextensionviewclosed=${this.#onExtensionViewClosed.bind(this)}
1503
- ${UI.Widget.widgetRef(RecordingView, widget => {this.#recordingView = widget;})}
1504
- ></devtools-widget>
1505
- `;
1506
- // clang-format on
1507
- }
1508
-
1509
- #renderCreateRecordingPage(): Lit.TemplateResult {
1510
- // clang-format off
1511
- return html`
1512
- <devtools-widget
1513
- class="recording-view"
1514
- ${widget(CreateRecordingView, {
1515
- recorderSettings: this.#recorderSettings,
1516
- onRecordingStarted: this.#onRecordingStarted.bind(this),
1517
- onRecordingCancelled: this.onRecordingCancelled.bind(this),
1518
- })}
1519
- ${UI.Widget.widgetRef(
1520
- CreateRecordingView,
1521
- widget => {
1522
- this.#createRecordingView = widget;
1523
- },
1524
- )}
1525
- ></devtools-widget>
1526
- `;
1527
- // clang-format on
1528
- }
1529
-
1530
- #getExportMenuButton = (): Buttons.Button.Button => {
1531
- if (!this.#exportMenuButton) {
1532
- throw new Error('#exportMenuButton not found');
1533
- }
1534
- return this.#exportMenuButton;
1535
- };
1536
-
1537
- #onExportRecording(event: Event): void {
1538
- event.stopPropagation();
1539
- this.#clearError();
1540
- this.exportMenuExpanded = !this.exportMenuExpanded;
1541
- }
1542
-
1543
- #onExportMenuClosed(): void {
1544
- this.exportMenuExpanded = false;
1545
- }
1546
-
1547
- override performUpdate(): void {
1548
- // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
1549
- Lit.render(this.render(), this.contentElement);
1550
- }
1551
-
1552
- protected render(): Lit.TemplateResult {
1553
- const recordings = this.#storage.getRecordings();
1554
- const selectValue: string = this.currentRecording ? this.currentRecording.storageName : this.currentPage;
1555
- // clang-format off
1556
- const values = [
1557
- recordings.length === 0
1558
- ? {
1559
- value: Pages.START_PAGE,
1560
- name: i18nString(UIStrings.noRecordings),
1561
- selected: selectValue === Pages.START_PAGE,
1562
- }
1563
- : {
1564
- value: Pages.ALL_RECORDINGS_PAGE,
1565
- name: `${recordings.length} ${i18nString(UIStrings.numberOfRecordings)}`,
1566
- selected: selectValue === Pages.ALL_RECORDINGS_PAGE,
1567
- },
1568
- ...recordings.map(recording => ({
1569
- value: recording.storageName,
1570
- name: recording.flow.title,
1571
- selected: selectValue === recording.storageName,
1572
- })),
1573
- ];
1574
-
1575
- return html`
1576
- <style>${UI.inspectorCommonStyles}</style>
1577
- <style>${recorderControllerStyles}</style>
1578
- <div class="wrapper">
1579
- <div class="header" jslog=${VisualLogging.toolbar()}>
1580
- <devtools-button
1581
- @click=${this.#onCreateNewRecording.bind(this)}
1582
- .data=${
1583
- {
1584
- variant: Buttons.Button.Variant.TOOLBAR,
1585
- iconName: 'plus',
1586
- disabled:
1587
- this.#replayState.isPlaying ||
1588
- this.isRecording ||
1589
- this.isToggling,
1590
- title: Models.Tooltip.getTooltipForActions(
1591
- i18nString(UIStrings.createRecording),
1592
- Actions.RecorderActions.CREATE_RECORDING,
1593
- ),
1594
- jslogContext: Actions.RecorderActions.CREATE_RECORDING,
1595
- } as Buttons.Button.ButtonData
1596
- }
1597
- ></devtools-button>
1598
- <div class="separator"></div>
1599
- <select
1600
- .disabled=${
1601
- recordings.length === 0 ||
1602
- this.#replayState.isPlaying ||
1603
- this.isRecording ||
1604
- this.isToggling
1605
- }
1606
- @click=${(e: Event) => e.stopPropagation()}
1607
- @change=${this.#onRecordingSelected.bind(this)}
1608
- jslog=${VisualLogging.dropDown('recordings').track({change: true})}
1609
- >
1610
- ${Lit.Directives.repeat(
1611
- values,
1612
- item => item.value,
1613
- item => {
1614
- return html`<option .selected=${item.selected} value=${item.value}>${item.name}</option>`;
1615
- },
1616
- )}
1617
- </select>
1618
- <div class="separator"></div>
1619
- <devtools-button
1620
- @click=${this.#onImportRecording.bind(this)}
1621
- .data=${
1622
- {
1623
- variant: Buttons.Button.Variant.TOOLBAR,
1624
- iconName: 'import',
1625
- title: i18nString(UIStrings.importRecording),
1626
- jslogContext: 'import-recording',
1627
- } as Buttons.Button.ButtonData
1628
- }
1629
- ></devtools-button>
1630
- <devtools-button
1631
- id='origin'
1632
- @click=${this.#onExportRecording.bind(this)}
1633
- ${ref(el => {
1634
- if (el instanceof HTMLElement) {
1635
- this.#exportMenuButton = el as Buttons.Button.Button;
1636
- }
1637
- })}
1638
- .data=${
1639
- {
1640
- variant: Buttons.Button.Variant.TOOLBAR,
1641
- iconName: 'download',
1642
- title: i18nString(UIStrings.exportRecording),
1643
- disabled: !this.currentRecording,
1644
- } as Buttons.Button.ButtonData
1645
- }
1646
- jslog=${VisualLogging.dropDown('export-recording').track({click: true})}
1647
- ></devtools-button>
1648
- <devtools-menu
1649
- @menucloserequest=${this.#onExportMenuClosed.bind(this)}
1650
- @menuitemselected=${this.#onExportOptionSelected.bind(this)}
1651
- .origin=${this.#getExportMenuButton}
1652
- .showDivider=${false}
1653
- .showSelectedItem=${false}
1654
- .open=${this.exportMenuExpanded}
1655
- >
1656
- <devtools-menu-group .name=${i18nString(UIStrings.export)}>
1657
- ${Lit.Directives.repeat(
1658
- this.#builtInConverters,
1659
- converter => {
1660
- return html`
1661
- <devtools-menu-item
1662
- .value=${converter.getId()}
1663
- jslog=${VisualLogging.item(`converter-${Platform.StringUtilities.toKebabCase(converter.getId())}`).track({click: true})}>
1664
- ${converter.getFormatName()}
1665
- </devtools-menu-item>
1666
- `;
1667
- },
1668
- )}
1669
- </devtools-menu-group>
1670
- <devtools-menu-group .name=${i18nString(UIStrings.exportViaExtensions)}>
1671
- ${Lit.Directives.repeat(
1672
- this.extensionConverters,
1673
- converter => {
1674
- return html`
1675
- <devtools-menu-item
1676
- .value=${converter.getId()}
1677
- jslog=${VisualLogging.item('converter-extension').track({click: true})}>
1678
- ${converter.getFormatName()}
1679
- </devtools-menu-item>
1680
- `;
1681
- },
1682
- )}
1683
- <devtools-menu-item .value=${GET_EXTENSIONS_MENU_ITEM}>
1684
- ${i18nString(UIStrings.getExtensions)}
1685
- </devtools-menu-item>
1686
- </devtools-menu-group>
1687
- </devtools-menu>
1688
- <devtools-button
1689
- @click=${this.#onDeleteRecording.bind(this)}
1690
- .data=${
1691
- {
1692
- variant: Buttons.Button.Variant.TOOLBAR,
1693
- iconName: 'bin',
1694
- disabled:
1695
- !this.currentRecording ||
1696
- this.#replayState.isPlaying ||
1697
- this.isRecording ||
1698
- this.isToggling,
1699
- title: i18nString(UIStrings.deleteRecording),
1700
- jslogContext: 'delete-recording',
1701
- } as Buttons.Button.ButtonData
1702
- }
1703
- ></devtools-button>
1704
- <div class="separator"></div>
1705
- <devtools-button
1706
- @click=${() => this.recordingPlayer?.continue()}
1707
- .data=${
1708
- {
1709
- variant: Buttons.Button.Variant.PRIMARY_TOOLBAR,
1710
- iconName: 'resume',
1711
- disabled:
1712
- !this.recordingPlayer ||
1713
- !this.#replayState.isPausedOnBreakpoint,
1714
- title: i18nString(UIStrings.continueReplay),
1715
- jslogContext: 'continue-replay',
1716
- } as Buttons.Button.ButtonData
1717
- }
1718
- ></devtools-button>
1719
- <devtools-button
1720
- @click=${() => this.recordingPlayer?.stepOver()}
1721
- .data=${
1722
- {
1723
- variant: Buttons.Button.Variant.TOOLBAR,
1724
- iconName: 'step-over',
1725
- disabled:
1726
- !this.recordingPlayer ||
1727
- !this.#replayState.isPausedOnBreakpoint,
1728
- title: i18nString(UIStrings.stepOverReplay),
1729
- jslogContext: 'step-over',
1730
- } as Buttons.Button.ButtonData
1731
- }
1732
- ></devtools-button>
1733
- <div class="feedback">
1734
- <devtools-link class="devtools-link" title=${i18nString(UIStrings.sendFeedback)} href=${
1735
- FEEDBACK_URL
1736
- } jslogcontext="feedback">${i18nString(UIStrings.sendFeedback)}</devtools-link>
1737
- </div>
1738
- <div class="separator"></div>
1739
- <devtools-shortcut-dialog
1740
- .data=${
1741
- {
1742
- shortcuts: this.#getShortcutsInfo(),
1743
- } as Dialogs.ShortcutDialog.ShortcutDialogData
1744
- } jslog=${VisualLogging.action('show-shortcuts').track({click: true})}
1745
- ></devtools-shortcut-dialog>
1746
- </div>
1747
- ${
1748
- this.importError
1749
- ? html`<div class='error'>Import error: ${
1750
- this.importError.message
1751
- }</div>`
1752
- : ''
1753
- }
1754
- ${this.#renderCurrentPage()}
1755
- </div>
1756
- `;
1757
- // clang-format on
1758
- }
1759
- }