UNPKG

chrome-devtools-frontend

Version:
240 lines (206 loc) • 11.4 kB
// Copyright 2022 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Bindings from '../../models/bindings/bindings.js'; import * as Breakpoints from '../../models/breakpoints/breakpoints.js'; import * as Persistence from '../../models/persistence/persistence.js'; import * as Workspace from '../../models/workspace/workspace.js'; import {renderElementIntoDOM} from '../../testing/DOMHelpers.js'; import { createTarget, describeWithEnvironment, } from '../../testing/EnvironmentHelpers.js'; import {describeWithMockConnection} from '../../testing/MockConnection.js'; import { createContentProviderUISourceCodes, createFileSystemUISourceCode, } from '../../testing/UISourceCodeHelpers.js'; import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as SourcesComponents from './components/components.js'; import * as Sources from './sources.js'; const {urlString} = Platform.DevToolsPath; describeWithEnvironment('SourcesView', () => { beforeEach(async () => { const actionRegistryInstance = UI.ActionRegistry.ActionRegistry.instance({forceNew: true}); const workspace = Workspace.Workspace.WorkspaceImpl.instance(); const targetManager = SDK.TargetManager.TargetManager.instance(); const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace); const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({ forceNew: true, resourceMapping, targetManager, }); const breakpointManager = Breakpoints.BreakpointManager.BreakpointManager.instance( {forceNew: true, targetManager, workspace, debuggerWorkspaceBinding}); Persistence.Persistence.PersistenceImpl.instance({forceNew: true, workspace, breakpointManager}); Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance({forceNew: true, workspace}); UI.ShortcutRegistry.ShortcutRegistry.instance({forceNew: true, actionRegistry: actionRegistryInstance}); }); it('creates new source view of updated type when renamed file requires a different viewer', async () => { const sourcesView = new Sources.SourcesView.SourcesView(); renderElementIntoDOM(sourcesView); const workspace = Workspace.Workspace.WorkspaceImpl.instance(); const {uiSourceCode, project} = createFileSystemUISourceCode({ url: urlString`file:///path/to/overrides/example.html`, mimeType: 'text/html', }); project.canSetFileContent = () => true; project.rename = (_uiSourceCode: Workspace.UISourceCode.UISourceCode, newName: string, callback: ( arg0: boolean, arg1?: string, arg2?: Platform.DevToolsPath.UrlString, arg3?: Common.ResourceType.ResourceType) => void) => { const newURL = urlString`${'file:///path/to/overrides/' + newName}`; let newContentType = Common.ResourceType.resourceTypes.Document; if (newName.endsWith('.jpg')) { newContentType = Common.ResourceType.resourceTypes.Image; } else if (newName.endsWith('.woff')) { newContentType = Common.ResourceType.resourceTypes.Font; } callback(true, newName, newURL, newContentType); }; sourcesView.viewForFile(uiSourceCode); assert.instanceOf(sourcesView.getSourceView(uiSourceCode), Sources.UISourceCodeFrame.UISourceCodeFrame); // Rename, but contentType stays the same await uiSourceCode.rename('newName.html' as Platform.DevToolsPath.RawPathString); assert.instanceOf(sourcesView.getSourceView(uiSourceCode), Sources.UISourceCodeFrame.UISourceCodeFrame); // Rename which changes contentType await uiSourceCode.rename('image.jpg' as Platform.DevToolsPath.RawPathString); assert.instanceOf(sourcesView.getSourceView(uiSourceCode), SourceFrame.ImageView.ImageView); // Rename which changes contentType await uiSourceCode.rename('font.woff' as Platform.DevToolsPath.RawPathString); assert.instanceOf(sourcesView.getSourceView(uiSourceCode), SourceFrame.FontView.FontView); workspace.removeProject(project); sourcesView.detach(); }); it('creates a HeadersView when the filename is \'.headers\'', async () => { const sourcesView = new Sources.SourcesView.SourcesView(); const uiSourceCode = new Workspace.UISourceCode.UISourceCode( {} as Persistence.FileSystemWorkspaceBinding.FileSystem, urlString`file:///path/to/overrides/www.example.com/.headers`, Common.ResourceType.resourceTypes.Document); sinon.stub(uiSourceCode, 'mimeType').returns('text/plain'); sourcesView.viewForFile(uiSourceCode); assert.instanceOf(sourcesView.getSourceView(uiSourceCode), SourcesComponents.HeadersView.HeadersView); }); it('shows and hides an infobar which warns about AI-generated changes', async () => { const attachSpy = sinon.spy(Sources.AiWarningInfobarPlugin.AiWarningInfobarPlugin.prototype, 'attachInfobar'); const removeSpy = sinon.spy(Sources.AiWarningInfobarPlugin.AiWarningInfobarPlugin.prototype, 'removeInfobar'); const sourcesView = new Sources.SourcesView.SourcesView(); const {uiSourceCode} = createFileSystemUISourceCode({ url: urlString`file:///path/to/project/example.ts`, mimeType: 'text/typescript', content: 'export class Foo {}', }); // Mock an AI-generated edit uiSourceCode.setWorkingCopy('export class Bar {}'); uiSourceCode.setContainsAiChanges(true); const contentLoadedPromise = new Promise(res => window.addEventListener('source-file-loaded', res)); const widget = sourcesView.viewForFile(uiSourceCode); assert.instanceOf(widget, Sources.UISourceCodeFrame.UISourceCodeFrame); const uiSourceCodeFrame = widget; // Only load the AiWarningInfobarPlugin sinon.stub(Sources.UISourceCodeFrame.UISourceCodeFrame, 'sourceFramePlugins').returns([ Sources.AiWarningInfobarPlugin.AiWarningInfobarPlugin ]); uiSourceCodeFrame.wasShown(); await contentLoadedPromise; sinon.assert.called(attachSpy); sinon.assert.notCalled(removeSpy); uiSourceCode.commitWorkingCopy(); sinon.assert.called(removeSpy); }); describe('viewForFile', () => { it('records the correct media type in the DevTools.SourcesPanelFileOpened metric', async () => { const sourcesView = new Sources.SourcesView.SourcesView(); const {uiSourceCode} = createFileSystemUISourceCode({ url: urlString`file:///path/to/project/example.ts`, mimeType: 'text/typescript', content: 'export class Foo {}', }); const sourcesPanelFileOpenedSpy = sinon.spy(Host.userMetrics, 'sourcesPanelFileOpened'); const contentLoadedPromise = new Promise(res => window.addEventListener('source-file-loaded', res)); const widget = sourcesView.viewForFile(uiSourceCode); assert.instanceOf(widget, Sources.UISourceCodeFrame.UISourceCodeFrame); const uiSourceCodeFrame = widget; // Skip creating the DebuggerPlugin, which times out and simulate DOM attach/showing. sinon.stub(uiSourceCodeFrame, 'loadPlugins' as keyof typeof uiSourceCodeFrame).callsFake(() => {}); uiSourceCodeFrame.wasShown(); await contentLoadedPromise; sinon.assert.calledWithExactly(sourcesPanelFileOpenedSpy, 'text/typescript'); }); }); }); describeWithMockConnection('SourcesView', () => { let target1: SDK.Target.Target; let target2: SDK.Target.Target; beforeEach(() => { const actionRegistryInstance = UI.ActionRegistry.ActionRegistry.instance({forceNew: true}); UI.ShortcutRegistry.ShortcutRegistry.instance({forceNew: true, actionRegistry: actionRegistryInstance}); target1 = createTarget(); target2 = createTarget(); const targetManager = target1.targetManager(); targetManager.setScopeTarget(target1); const workspace = Workspace.Workspace.WorkspaceImpl.instance(); const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace); Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance({forceNew: true, resourceMapping, targetManager}); const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({ forceNew: true, resourceMapping, targetManager, }); Bindings.IgnoreListManager.IgnoreListManager.instance({forceNew: true, debuggerWorkspaceBinding}); const breakpointManager = Breakpoints.BreakpointManager.BreakpointManager.instance( {forceNew: true, targetManager, workspace, debuggerWorkspaceBinding}); Persistence.Persistence.PersistenceImpl.instance({forceNew: true, workspace, breakpointManager}); Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance({forceNew: true, workspace}); }); it('creates editor tabs only for in-scope uiSourceCodes', () => { const addUISourceCodeSpy = sinon.spy(Sources.TabbedEditorContainer.TabbedEditorContainer.prototype, 'addUISourceCode'); const removeUISourceCodesSpy = sinon.spy(Sources.TabbedEditorContainer.TabbedEditorContainer.prototype, 'removeUISourceCodes'); createContentProviderUISourceCodes({ items: [ {url: urlString`http://example.com/a.js`, mimeType: 'application/javascript'}, {url: urlString`http://example.com/b.js`, mimeType: 'application/javascript'}, ], projectId: 'projectId1', projectType: Workspace.Workspace.projectTypes.Network, target: target1, }); createContentProviderUISourceCodes({ items: [ {url: urlString`http://foo.com/script.js`, mimeType: 'application/javascript'}, ], projectId: 'projectId2', projectType: Workspace.Workspace.projectTypes.Network, target: target2, }); new Sources.SourcesView.SourcesView(); let addedURLs = addUISourceCodeSpy.args.map(args => args[0].url()); assert.deepEqual(addedURLs, ['http://example.com/a.js', 'http://example.com/b.js']); sinon.assert.notCalled(removeUISourceCodesSpy); addUISourceCodeSpy.resetHistory(); target2.targetManager().setScopeTarget(target2); addedURLs = addUISourceCodeSpy.args.map(args => args[0].url()); assert.deepEqual(addedURLs, ['http://foo.com/script.js']); const removedURLs = removeUISourceCodesSpy.args.map(args => args[0][0].url()); assert.deepEqual(removedURLs, ['http://example.com/a.js', 'http://example.com/b.js']); }); it('doesn\'t remove non-network UISourceCodes when changing the scope target', () => { createFileSystemUISourceCode({ url: urlString`snippet:///foo.js`, mimeType: 'application/javascript', type: Persistence.PlatformFileSystem.PlatformFileSystemType.SNIPPETS, }); const sourcesView = new Sources.SourcesView.SourcesView(); const removeUISourceCodesSpy = sinon.spy(sourcesView.editorContainer, 'removeUISourceCodes'); target2.targetManager().setScopeTarget(target2); sinon.assert.notCalled(removeUISourceCodesSpy); }); });