chrome-devtools-frontend 1.0.1662289 → 1.0.1664496
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.
- package/.agents/skills/{creating-a-model → devtools-model-management}/SKILL.md +0 -1
- package/.agents/skills/devtools-unicode-escaping/SKILL.md +64 -0
- package/.agents/skills/devtools-ux-writing-refactor/SKILL.md +2 -1
- package/.agents/skills/fixing-skipped-tests/SKILL.md +3 -3
- package/.agents/skills/merging-devtools-module/SKILL.md +1 -1
- package/.agents/skills/migrate-chromium-test/SKILL.md +32 -6
- package/.agents/skills/repro-flaky-tests/SKILL.md +71 -31
- package/.agents/skills/ui-eng-vision-widget-promoter/SKILL.md +1 -0
- package/docs/ui_engineering.md +2 -0
- package/eslint.config.mjs +1 -0
- package/front_end/core/common/SettingRegistration.ts +0 -5
- package/front_end/core/common/Settings.ts +144 -3
- package/front_end/core/platform/StringUtilities.ts +38 -6
- package/front_end/core/protocol_client/PuppeteerDevToolsConnection.ts +112 -0
- package/front_end/core/sdk/CPUProfilerModel.ts +21 -13
- package/front_end/core/sdk/CSSContainerQuery.ts +1 -1
- package/front_end/core/sdk/CSSLayer.ts +1 -1
- package/front_end/core/sdk/CSSMedia.ts +1 -1
- package/front_end/core/sdk/CSSModel.ts +1 -1
- package/front_end/core/sdk/CSSNavigation.ts +1 -1
- package/front_end/core/sdk/CSSProperty.ts +1 -1
- package/front_end/core/sdk/CSSQuery.ts +1 -1
- package/front_end/core/sdk/CSSRule.ts +1 -1
- package/front_end/core/sdk/CSSScope.ts +1 -1
- package/front_end/core/sdk/CSSStartingStyle.ts +1 -1
- package/front_end/core/sdk/CSSStyleDeclaration.ts +1 -1
- package/front_end/core/sdk/CSSStyleSheetHeader.ts +1 -1
- package/front_end/core/sdk/CSSSupports.ts +1 -1
- package/front_end/core/sdk/CompilerSourceMappingContentProvider.ts +1 -1
- package/front_end/core/sdk/NetworkManager.ts +1 -1
- package/front_end/core/sdk/NetworkRequest.ts +7 -7
- package/front_end/core/sdk/Resource.ts +1 -1
- package/front_end/core/sdk/ScopeTreeCache.ts +1 -1
- package/front_end/core/sdk/Script.ts +1 -1
- package/front_end/core/sdk/ServerSentEvents.ts +1 -1
- package/front_end/core/sdk/ServiceWorkerCacheModel.ts +0 -10
- package/front_end/core/sdk/SourceMap.ts +1 -1
- package/front_end/core/sdk/SourceMapScopesInfo.ts +1 -1
- package/front_end/core/sdk/StorageBucketsModel.ts +0 -10
- package/front_end/core/sdk/sdk-meta.ts +3 -4
- package/front_end/{models → core}/text_utils/ContentProvider.ts +4 -6
- package/front_end/{models → core}/text_utils/StaticContentProvider.ts +4 -6
- package/front_end/{models → core}/text_utils/StreamingContentData.ts +1 -2
- package/front_end/{models → core}/text_utils/TextRange.ts +6 -3
- package/front_end/{models → core}/text_utils/TextUtils.ts +7 -7
- package/front_end/{models → core}/text_utils/WasmDisassembly.ts +2 -2
- package/front_end/core/text_utils/text_utils.ts +27 -0
- package/front_end/entrypoints/formatter_worker/AcornTokenizer.ts +1 -1
- package/front_end/entrypoints/heap_snapshot_worker/HeapSnapshotLoader.ts +1 -1
- package/front_end/entrypoints/inspector_main/OutermostTargetSelector.ts +0 -13
- package/front_end/entrypoints/inspector_main/RenderingOptions.ts +8 -3
- package/front_end/entrypoints/inspector_main/inspector_main-meta.ts +3 -3
- package/front_end/entrypoints/main/MainImpl.ts +21 -3
- package/front_end/foundation/Universe.ts +16 -0
- package/front_end/generated/InspectorBackendCommands.ts +2 -14
- package/front_end/generated/protocol-mapping.d.ts +0 -53
- package/front_end/generated/protocol-proxy-api.d.ts +0 -46
- package/front_end/generated/protocol.ts +0 -148
- package/front_end/models/ai_assistance/AgentProject.ts +1 -1
- package/front_end/models/ai_assistance/AiAgent2.ts +3 -3
- package/front_end/models/ai_assistance/AiConversation.ts +21 -10
- package/front_end/models/ai_assistance/AiHistoryStorage.ts +21 -10
- package/front_end/models/ai_assistance/ChangeManager.ts +2 -2
- package/front_end/models/ai_assistance/ExtensionScope.ts +6 -2
- package/front_end/models/ai_assistance/agents/AccessibilityAgent.ts +4 -7
- package/front_end/models/ai_assistance/agents/AiAgent.ts +9 -2
- package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +19 -11
- package/front_end/models/ai_assistance/agents/PerformanceAgent.snapshot.txt +2 -2
- package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +23 -10
- package/front_end/models/ai_assistance/agents/StorageAgent.ts +19 -18
- package/front_end/models/ai_assistance/agents/StylingAgent.ts +3 -4
- package/front_end/models/ai_assistance/contexts/FileContext.ts +7 -3
- package/front_end/models/ai_assistance/contexts/PerformanceTraceContext.ts +39 -11
- package/front_end/models/ai_assistance/data_formatters/FileFormatter.ts +6 -3
- package/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.ts +1 -1
- package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.snapshot.txt +4 -4
- package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +8 -8
- package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts +76 -3
- package/front_end/models/ai_assistance/tools/ListNetworkRequests.ts +11 -1
- package/front_end/models/badges/AiExplorerBadge.ts +8 -3
- package/front_end/models/badges/Badge.ts +1 -0
- package/front_end/models/badges/UserBadges.ts +43 -23
- package/front_end/models/bindings/CompilerScriptMapping.ts +1 -1
- package/front_end/models/bindings/ContentProviderBasedProject.ts +1 -1
- package/front_end/models/bindings/DebuggerLanguagePlugins.ts +2 -2
- package/front_end/models/bindings/DebuggerWorkspaceBinding.ts +1 -1
- package/front_end/models/bindings/DefaultScriptMapping.ts +1 -1
- package/front_end/models/bindings/FileUtils.ts +1 -1
- package/front_end/models/bindings/NetworkProject.ts +16 -4
- package/front_end/models/bindings/PresentationConsoleMessageHelper.ts +7 -10
- package/front_end/models/bindings/ResourceMapping.ts +1 -1
- package/front_end/models/bindings/ResourceScriptMapping.ts +1 -1
- package/front_end/models/bindings/SASSSourceMapping.ts +1 -1
- package/front_end/models/bindings/StylesSourceMapping.ts +1 -1
- package/front_end/models/breakpoints/BreakpointManager.ts +1 -1
- package/front_end/models/crux-manager/CrUXManager.ts +1 -1
- package/front_end/models/emulation/DeviceModeModel.ts +67 -5
- package/front_end/models/emulation/EmulatedDevices.ts +3 -2
- package/front_end/models/extensions/RecorderPluginManager.ts +10 -6
- package/front_end/models/har/Importer.ts +1 -1
- package/front_end/models/har/Log.snapshot.txt +193 -0
- package/front_end/models/har/Log.ts +3 -2
- package/front_end/models/har/Writer.ts +1 -1
- package/front_end/models/issues_manager/IssueAggregator.ts +9 -0
- package/front_end/models/issues_manager/IssuesManager.ts +5 -0
- package/front_end/models/issues_manager/LazyLoadImageIssue.ts +76 -0
- package/front_end/models/issues_manager/descriptions/lazyLoadImageZeroSize.md +7 -0
- package/front_end/models/issues_manager/issues_manager.ts +2 -0
- package/front_end/models/javascript_metadata/NativeFunctions.js +9 -10
- package/front_end/models/live-metrics/LiveMetrics.ts +2 -3
- package/front_end/models/logs/NetworkLog.ts +4 -3
- package/front_end/models/logs/logs-meta.ts +3 -3
- package/front_end/models/network_time_calculator/NetworkTimeCalculator.ts +4 -4
- package/front_end/models/persistence/Automapping.ts +1 -1
- package/front_end/models/persistence/AutomaticFileSystemWorkspaceBinding.ts +2 -2
- package/front_end/models/persistence/FileSystemWorkspaceBinding.ts +1 -1
- package/front_end/models/persistence/IsolatedFileSystem.ts +2 -2
- package/front_end/models/persistence/NetworkPersistenceManager.ts +1 -1
- package/front_end/models/persistence/PersistenceImpl.ts +1 -1
- package/front_end/models/persistence/PlatformFileSystem.ts +1 -1
- package/front_end/models/source_map_scopes/FunctionCodeResolver.ts +8 -6
- package/front_end/models/source_map_scopes/NamesResolver.ts +37 -31
- package/front_end/models/stack_trace/StackTrace.ts +17 -0
- package/front_end/models/text_utils/text_utils.ts +2 -24
- package/front_end/models/trace/insights/ImageDelivery.ts +2 -2
- package/front_end/models/trace/insights/LegacyJavaScript.ts +1 -1
- package/front_end/models/trace/insights/RenderBlocking.ts +1 -1
- package/front_end/models/trace/insights/ThirdParties.ts +1 -1
- package/front_end/models/workspace/FileManager.ts +1 -1
- package/front_end/models/workspace/UISourceCode.ts +1 -1
- package/front_end/models/workspace/WorkspaceImpl.ts +1 -1
- package/front_end/models/workspace_diff/WorkspaceDiff.ts +1 -1
- package/front_end/panels/accessibility/AccessibilityNodeView.ts +1 -1
- package/front_end/panels/accessibility/AccessibilityStrings.ts +2 -2
- package/front_end/panels/ai_assistance/AiAssistancePanel.ts +1 -1
- package/front_end/panels/ai_assistance/ExportConversation.ts +1 -1
- package/front_end/panels/ai_assistance/ai_assistance-meta.ts +1 -1
- package/front_end/panels/ai_assistance/components/ChatMessage.ts +1 -1
- package/front_end/panels/application/AppManifestView.ts +4 -4
- package/front_end/panels/application/ApplicationPanelSidebar.ts +1 -2
- package/front_end/panels/application/CrashReportContextView.ts +1 -1
- package/front_end/panels/application/DOMStorageItemsView.ts +1 -1
- package/front_end/panels/application/DeviceBoundSessionsView.ts +4 -4
- package/front_end/panels/application/ExtensionStorageItemsView.ts +1 -1
- package/front_end/panels/application/FrameDetailsView.ts +1 -1
- package/front_end/panels/application/IndexedDBModel.ts +0 -10
- package/front_end/panels/application/IndexedDBViews.ts +240 -164
- package/front_end/panels/application/ServiceWorkerCacheTreeElement.ts +12 -2
- package/front_end/panels/application/ServiceWorkerCacheViews.ts +1 -1
- package/front_end/panels/application/SharedStorageModel.ts +0 -10
- package/front_end/panels/application/application.ts +2 -0
- package/front_end/panels/application/components/AdsView.ts +23 -0
- package/front_end/panels/application/components/BackForwardCacheView.ts +21 -5
- package/front_end/panels/application/components/CrashReportContextGrid.ts +1 -1
- package/front_end/panels/application/components/SharedStorageAccessGrid.ts +1 -1
- package/front_end/panels/application/components/TrustTokensView.ts +2 -2
- package/front_end/panels/application/components/adsView.css +21 -1
- package/front_end/panels/application/indexedDBViews.css +1 -1
- package/front_end/panels/application/preloading/PreloadingView.ts +1 -1
- package/front_end/panels/application/preloading/components/PreloadingDisabledInfobar.ts +2 -2
- package/front_end/panels/application/preloading/components/PreloadingString.ts +2 -2
- package/front_end/panels/application/preloading/components/UsedPreloadingView.ts +4 -4
- package/front_end/panels/autofill/AutofillView.ts +1 -1
- package/front_end/panels/common/AiCodeCompletionTeaser.ts +1 -1
- package/front_end/panels/common/ExtensionServer.ts +1 -1
- package/front_end/panels/console/ConsoleFilter.ts +1 -1
- package/front_end/panels/console/ConsoleSidebar.ts +1 -1
- package/front_end/panels/console/ConsoleView.ts +1 -1
- package/front_end/panels/console/ConsoleViewMessage.ts +1 -1
- package/front_end/panels/console/PromptBuilder.ts +1 -1
- package/front_end/panels/coverage/CoverageDecorationManager.ts +9 -1
- package/front_end/panels/coverage/CoverageModel.ts +1 -1
- package/front_end/panels/css_overview/CSSOverviewCompletedView.ts +1 -1
- package/front_end/panels/developer_resources/DeveloperResourcesListView.ts +1 -1
- package/front_end/panels/developer_resources/DeveloperResourcesView.ts +1 -1
- package/front_end/panels/elements/AccessibilityTreeView.ts +38 -3
- package/front_end/panels/elements/AdoptedStyleSheetTreeElement.ts +1 -1
- package/front_end/panels/elements/ElementIssueUtils.ts +2 -2
- package/front_end/panels/elements/ElementsTreeElement.ts +10 -3
- package/front_end/panels/elements/StandaloneStylesContainer.ts +1 -1
- package/front_end/panels/elements/StylePropertiesSection.ts +1 -1
- package/front_end/panels/elements/StylePropertyTreeElement.ts +1 -1
- package/front_end/panels/elements/StylesAiCodeCompletionProvider.ts +1 -1
- package/front_end/panels/elements/StylesContainer.ts +1 -1
- package/front_end/panels/elements/StylesSidebarPane.ts +1 -1
- package/front_end/panels/elements/components/CSSPropertyDocsView.ts +1 -1
- package/front_end/panels/explain/components/ConsoleInsight.ts +2 -2
- package/front_end/panels/issues/AffectedLazyLoadImagesView.ts +76 -0
- package/front_end/panels/issues/AffectedResourcesView.ts +1 -1
- package/front_end/panels/issues/CorsIssueDetailsView.ts +1 -1
- package/front_end/panels/issues/IssueView.ts +2 -0
- package/front_end/panels/issues/issues.ts +2 -0
- package/front_end/panels/layer_viewer/Layers3DView.ts +1 -1
- package/front_end/panels/lighthouse/LighthouseController.ts +5 -0
- package/front_end/panels/lighthouse/LighthouseReportRenderer.ts +1 -1
- package/front_end/panels/network/BinaryResourceView.ts +1 -1
- package/front_end/panels/network/NetworkDataGridNode.ts +72 -8
- package/front_end/panels/network/NetworkLogView.ts +1 -1
- package/front_end/panels/network/NetworkLogViewColumns.ts +6 -6
- package/front_end/panels/network/NetworkPanel.ts +4 -4
- package/front_end/panels/network/NetworkSearchScope.ts +1 -1
- package/front_end/panels/network/RequestConditionsDrawer.ts +1 -1
- package/front_end/panels/network/RequestHTMLView.ts +1 -1
- package/front_end/panels/network/RequestInitiatorView.ts +24 -2
- package/front_end/panels/network/RequestPayloadView.ts +1 -1
- package/front_end/panels/network/RequestPreviewView.ts +1 -1
- package/front_end/panels/network/RequestResponseView.ts +1 -1
- package/front_end/panels/network/ResourceChunkView.ts +1 -1
- package/front_end/panels/network/ResourceDirectSocketChunkView.ts +1 -1
- package/front_end/panels/network/ResourceWebSocketFrameView.ts +1 -1
- package/front_end/panels/network/components/ResponseHeaderSection.ts +1 -1
- package/front_end/panels/network/forward/UIRequestLocation.ts +1 -1
- package/front_end/panels/network/network-meta.ts +3 -3
- package/front_end/panels/network/networkLogView.css +5 -0
- package/front_end/panels/network/requestInitiatorViewTree.css +5 -0
- package/front_end/panels/profiler/HeapProfileView.ts +200 -81
- package/front_end/panels/profiler/HeapSnapshotView.ts +1 -1
- package/front_end/panels/profiler/ProfileDataGrid.ts +12 -8
- package/front_end/panels/protocol_monitor/ProtocolMonitor.ts +1 -1
- package/front_end/panels/recorder/README.md +1 -2
- package/front_end/panels/recorder/RecorderPanel.ts +1861 -17
- package/front_end/panels/recorder/RecordingListView.ts +8 -37
- package/front_end/panels/recorder/RecordingView.ts +54 -27
- package/front_end/panels/recorder/SelectorPicker.ts +0 -13
- package/front_end/panels/recorder/StepEditor.ts +6 -14
- package/front_end/panels/recorder/StepView.ts +36 -105
- package/front_end/panels/recorder/recorder.ts +0 -2
- package/front_end/panels/search/SearchResultsPane.ts +1 -1
- package/front_end/panels/security/SecurityPanel.ts +3 -3
- package/front_end/panels/settings/AISettingsTab.ts +17 -4
- package/front_end/panels/settings/components/SyncSection.ts +4 -4
- package/front_end/panels/settings/emulation/components/UserAgentClientHintsForm.ts +3 -3
- package/front_end/panels/snippets/ScriptSnippetFileSystem.ts +1 -1
- package/front_end/panels/sources/BreakpointsView.ts +1 -1
- package/front_end/panels/sources/CoveragePlugin.ts +1 -1
- package/front_end/panels/sources/DebuggerPlugin.ts +4 -4
- package/front_end/panels/sources/FilteredUISourceCodeListProvider.ts +27 -3
- package/front_end/panels/sources/NavigatorView.ts +1 -1
- package/front_end/panels/sources/PersistenceActions.ts +1 -1
- package/front_end/panels/sources/SourcesNavigator.ts +1 -1
- package/front_end/panels/sources/SourcesSearchScope.ts +1 -1
- package/front_end/panels/sources/TabbedEditorContainer.ts +3 -3
- package/front_end/panels/sources/UISourceCodeFrame.ts +1 -1
- package/front_end/panels/sources/components/HeadersView.ts +1 -1
- package/front_end/panels/sources/filteredUISourceCodeListProvider.css +12 -1
- package/front_end/panels/sources/sources-meta.ts +1 -1
- package/front_end/panels/timeline/StatusDialog.ts +1 -1
- package/front_end/panels/timeline/TimelineController.ts +24 -3
- package/front_end/panels/timeline/TimelinePanel.ts +116 -25
- package/front_end/panels/timeline/TimelineSelectorStatsView.ts +2 -2
- package/front_end/panels/timeline/TimelineUIUtils.ts +1 -1
- package/front_end/panels/timeline/components/CWVMetrics.ts +1 -1
- package/front_end/panels/timeline/components/IgnoreListSetting.ts +1 -1
- package/front_end/panels/timeline/overlays/components/EntryLabelOverlay.ts +2 -2
- package/front_end/panels/timeline/timeline-meta.ts +13 -0
- package/front_end/services/tracing/TracingManager.ts +27 -2
- package/front_end/third_party/chromium/README.chromium +1 -1
- package/front_end/third_party/lighthouse/README.chromium +2 -2
- package/front_end/third_party/lighthouse/lighthouse-dt-bundle.js +1418 -1413
- package/front_end/third_party/lighthouse/locales/en-US.json +12 -6
- package/front_end/third_party/lighthouse/locales/en-XL.json +12 -6
- package/front_end/third_party/lighthouse/report-assets/report-generator.mjs +1 -1
- package/front_end/third_party/puppeteer/puppeteer.ts +20 -1
- package/front_end/ui/components/highlighting/HighlightElement.ts +4 -1
- package/front_end/ui/components/highlighting/HighlightManager.ts +1 -1
- package/front_end/ui/components/highlighting/MarkupHighlight.ts +1 -1
- package/front_end/ui/components/markdown_view/MarkdownView.ts +46 -2
- package/front_end/ui/components/markdown_view/markdownView.css +23 -0
- package/front_end/ui/components/spinners/Spinner.ts +29 -32
- package/front_end/ui/components/spinners/spinner.css +32 -84
- package/front_end/ui/components/text_editor/config.ts +1 -1
- package/front_end/ui/components/tree_outline/TreeOutline.ts +7 -1
- package/front_end/ui/legacy/Infobar.ts +1 -1
- package/front_end/ui/legacy/InspectorView.ts +12 -7
- package/front_end/ui/legacy/SuggestBox.ts +1 -1
- package/front_end/ui/legacy/TextPrompt.ts +1 -1
- package/front_end/ui/legacy/Toolbar.ts +5 -2
- package/front_end/ui/legacy/Treeoutline.ts +40 -13
- package/front_end/ui/legacy/UIUtils.ts +25 -10
- package/front_end/ui/legacy/Widget.ts +17 -10
- package/front_end/ui/legacy/components/color_picker/Spectrum.ts +1 -1
- package/front_end/ui/legacy/components/data_grid/DataGridElement.ts +37 -8
- package/front_end/ui/legacy/components/data_grid/ViewportDataGrid.ts +1 -1
- package/front_end/ui/legacy/components/data_grid/dataGrid.css +1 -1
- package/front_end/ui/legacy/components/inline_editor/AnimationTimingUI.ts +27 -13
- package/front_end/ui/legacy/components/inline_editor/BezierEditor.ts +5 -2
- package/front_end/ui/legacy/components/inline_editor/CSSLinearEasingModel.ts +23 -0
- package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +223 -121
- package/front_end/ui/legacy/components/object_ui/RemoteObjectPreviewFormatter.ts +1 -1
- package/front_end/ui/legacy/components/object_ui/objectValue.css +2 -1
- package/front_end/ui/legacy/components/quick_open/FilteredListWidget.ts +1 -1
- package/front_end/ui/legacy/components/source_frame/BinaryResourceViewFactory.ts +1 -1
- package/front_end/ui/legacy/components/source_frame/FontView.ts +1 -1
- package/front_end/ui/legacy/components/source_frame/ImageView.ts +1 -1
- package/front_end/ui/legacy/components/source_frame/JSONView.ts +105 -115
- package/front_end/ui/legacy/components/source_frame/PreviewFactory.ts +1 -1
- package/front_end/ui/legacy/components/source_frame/ResourceSourceFrame.ts +1 -1
- package/front_end/ui/legacy/components/source_frame/SourceFrame.ts +3 -3
- package/front_end/ui/legacy/components/source_frame/StreamingContentHexView.ts +1 -1
- package/front_end/ui/legacy/components/source_frame/XMLView.ts +5 -1
- package/front_end/ui/legacy/components/source_frame/jsonView.css +4 -0
- package/front_end/ui/legacy/components/utils/Linkifier.ts +1 -1
- package/front_end/ui/legacy/inspectorCommon.css +10 -6
- package/front_end/ui/lit/lit.ts +1 -0
- package/front_end/ui/lit/render.ts +44 -10
- package/front_end/ui/lit/strip-whitespace.ts +23 -2
- package/front_end/ui/settings/SettingUIRegistration.ts +105 -0
- package/front_end/ui/settings/settings.ts +9 -0
- package/front_end/ui/visual_logging/KnownContextValues.ts +13 -0
- package/mcp/mcp.ts +1 -0
- package/package.json +1 -1
- package/front_end/panels/recorder/RecorderController.ts +0 -1759
- /package/.agents/skills/{verification → devtools-verification}/SKILL.md +0 -0
- /package/front_end/{models → core}/text_utils/CodeMirrorUtils.ts +0 -0
- /package/front_end/{models → core}/text_utils/ContentData.ts +0 -0
- /package/front_end/{models → core}/text_utils/Text.ts +0 -0
- /package/front_end/{models → core}/text_utils/TextCursor.ts +0 -0
- /package/front_end/{models → core}/text_utils/TextUtils.snapshot.txt +0 -0
- /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
|
-
}
|