diff --git a/front_end/panels/ai_assistance/PatchWidget.test.ts b/front_end/panels/ai_assistance/PatchWidget.test.ts index 26f7b0d461..a5cb549caf 100644 --- a/front_end/panels/ai_assistance/PatchWidget.test.ts +++ b/front_end/panels/ai_assistance/PatchWidget.test.ts @@ -12,7 +12,6 @@ import { cleanup, createPatchWidget, createPatchWidgetWithDiffView, - createTestFilesystem, initializePersistenceImplForTests, MockAidaAbortError, mockAidaClient, @@ -20,7 +19,7 @@ import { } from '../../testing/AiAssistanceHelpers.js'; import {updateHostConfig} from '../../testing/EnvironmentHelpers.js'; import {describeWithMockConnection} from '../../testing/MockConnection.js'; -import {createContentProviderUISourceCode} from '../../testing/UISourceCodeHelpers.js'; +import {createContentProviderUISourceCode, createFileSystemUISourceCode} from '../../testing/UISourceCodeHelpers.js'; import * as AiAssistance from './ai_assistance.js'; @@ -37,7 +36,7 @@ describeWithMockConnection('PatchWidget', () => { cleanup(); }); - describe('applyToWorkspace', () => { + describe('applyToPageTree', () => { beforeEach(() => { createContentProviderUISourceCode({ url: Platform.DevToolsPath.urlString`file://test/index.html`, @@ -129,7 +128,7 @@ describeWithMockConnection('PatchWidget', () => { }); }); - it('should show FRE dialog on applyToWorkspace click if the setting is false', async () => { + it('should show FRE dialog on applyToPageTree click if the setting is false', async () => { Common.Settings.moduleSetting('ai-assistance-patching-fre-completed').set(false); const {view, widget} = await createPatchWidget(); widget.changeSummary = 'body { background-color: red; }'; @@ -139,7 +138,7 @@ describeWithMockConnection('PatchWidget', () => { assert.isTrue(showFreDialogStub.called, 'Expected FreDialog to be shown but it\'s not shown'); }); - it('should not show FRE dialog on applyToWorkspace click if the setting is true', async () => { + it('should not show FRE dialog on applyToPageTree click if the setting is true', async () => { Common.Settings.moduleSetting('ai-assistance-patching-fre-completed').set(true); const {view, widget} = await createPatchWidget(); widget.changeSummary = 'body { background-color: red; }'; @@ -166,7 +165,7 @@ Files: * /index.html`); }); - it('should show error state when applyToWorkspace fails', async () => { + it('should show error state when applyToPageTree fails', async () => { const {view, widget} = await createPatchWidget({aidaClient: mockAidaClient([[MockAidaFetchError]])}); widget.changeSummary = 'body { background-color: red; }'; @@ -188,6 +187,7 @@ Files: }); describe('diff view', () => { + const origContent = 'window.foo = () => "foo";\n'; let fileSystemUISourceCode: Workspace.UISourceCode.UISourceCode; let commitWorkingCopyStub: sinon.SinonStub>; @@ -195,8 +195,6 @@ Files: sinon.SinonStub>; beforeEach(() => { - fileSystemUISourceCode = createTestFilesystem('file://test').uiSourceCode; - Common.Settings.Settings.instance().createSetting('ai-assistance-patching-selected-project-id', 'file://test'); updateHostConfig({ devToolsFreestyler: { enabled: true, @@ -204,12 +202,32 @@ Files: }, }); + const url = Platform.DevToolsPath.urlString`https://example.com/script.js`; + createContentProviderUISourceCode({ + url, + content: origContent, + mimeType: 'text/javascript', + projectType: Workspace.Workspace.projectTypes.Network, + metadata: new Workspace.UISourceCode.UISourceCodeMetadata(null, origContent.length), + }); + commitWorkingCopyStub = sinon.stub(Workspace.UISourceCode.UISourceCode.prototype, 'commitWorkingCopy').callThrough(); resetWorkingCopyStub = sinon.stub(Workspace.UISourceCode.UISourceCode.prototype, 'resetWorkingCopy').callThrough(); }); + const createBoundFileSystemUISourceCode = () => { + const localUrl = Platform.DevToolsPath.urlString`file:///var/www/script.js`; + ({uiSourceCode: fileSystemUISourceCode} = createFileSystemUISourceCode({ + url: localUrl, + mimeType: 'text/javascript', + content: origContent, + autoMapping: true, + metadata: new Workspace.UISourceCode.UISourceCodeMetadata(null, origContent.length), + })); + }; + it('on apply should call handle function and stash changes', async () => { const { view, @@ -223,22 +241,31 @@ Files: assert.isTrue(changeManager.stashChanges.calledOnce); }); - it('save all should commit the working copy of the changed UI codes to the disk and render savedToDisk view', - async () => { - const {view, widget} = await createPatchWidgetWithDiffView(); - const changeManager = sinon.createStubInstance(AiAssistanceModel.ChangeManager); - widget.changeManager = changeManager; - fileSystemUISourceCode.setWorkingCopy('working copy'); + it('"save to workspace" is not available if there is no matching file system mapping', async () => { + const {view, widget} = await createPatchWidgetWithDiffView(); + const changeManager = sinon.createStubInstance(AiAssistanceModel.ChangeManager); + widget.changeManager = changeManager; + assert.isUndefined(view.input.onSaveToWorkspace); + }); - view.input.onSaveAll(); - const nextInput = await view.nextInput; + it('"save to workspace" should commit the working copy of the files to disk and update the view', async () => { + createBoundFileSystemUISourceCode(); + fileSystemUISourceCode.setWorkingCopy('working copy'); + const {view, widget} = await createPatchWidgetWithDiffView(); + const changeManager = sinon.createStubInstance(AiAssistanceModel.ChangeManager); + widget.changeManager = changeManager; - assert.isTrue(nextInput.savedToDisk); - assert.isTrue(commitWorkingCopyStub.called, 'Expected commitWorkingCopy to be called but it is not called'); - assert.isTrue(changeManager.dropStashedChanges.calledOnce); - }); + assert.isDefined(view.input.onSaveToWorkspace); + view.input.onSaveToWorkspace(); + const nextInput = await view.nextInput; + + assert.isTrue(nextInput.savedToDisk); + assert.isTrue(commitWorkingCopyStub.called, 'Expected commitWorkingCopy to be called but it is not called'); + assert.isTrue(changeManager.dropStashedChanges.calledOnce); + }); it('discard should discard the working copy and render the view without patchSuggestion', async () => { + createBoundFileSystemUISourceCode(); const {view, widget} = await createPatchWidgetWithDiffView(); const changeManager = sinon.createStubInstance(AiAssistanceModel.ChangeManager); widget.changeManager = changeManager; diff --git a/front_end/panels/ai_assistance/PatchWidget.ts b/front_end/panels/ai_assistance/PatchWidget.ts index c97ffb5540..662ce4afb7 100644 --- a/front_end/panels/ai_assistance/PatchWidget.ts +++ b/front_end/panels/ai_assistance/PatchWidget.ts @@ -13,6 +13,7 @@ import * as i18n from '../../core/i18n/i18n.js'; import type * as Platform from '../../core/platform/platform.js'; import * as Root from '../../core/root/root.js'; import * as AiAssistanceModel from '../../models/ai_assistance/ai_assistance.js'; +import * as Persistence from '../../models/persistence/persistence.js'; import * as WorkspaceDiff from '../../models/workspace_diff/workspace_diff.js'; import * as Buttons from '../../ui/components/buttons/buttons.js'; import * as UI from '../../ui/legacy/legacy.js'; @@ -48,9 +49,9 @@ const UIStringsNotTranslate = { */ discard: 'Discard', /** - *@description Button text to save all the suggested changes to file system + *@description Button text to save all the suggested changes to the file system */ - saveAll: 'Save all', + saveToWorkspace: 'Save to workspace', /** *@description Header text after the user saved the changes to the disk. */ @@ -143,7 +144,7 @@ export interface ViewInput { onApplyToPageTree: () => void; onCancel: () => void; onDiscard: () => void; - onSaveAll: () => void; + onSaveToWorkspace?: () => void; } export interface ViewOutput { @@ -167,6 +168,7 @@ export class PatchWidget extends UI.Widget.Widget { #noLogging: boolean; // Whether the enterprise setting is `ALLOW_WITHOUT_LOGGING` or not. #patchSuggestionState = PatchSuggestionState.INITIAL; #workspaceDiff = WorkspaceDiff.WorkspaceDiff.workspaceDiff(); + #persistence = Persistence.Persistence.PersistenceImpl.instance(); constructor(element?: HTMLElement, view?: View, opts?: { aidaClient: Host.AidaClient.AidaClient, @@ -278,12 +280,14 @@ export class PatchWidget extends UI.Widget.Widget { .variant=${Buttons.Button.Variant.OUTLINED}> ${lockedString(UIStringsNotTranslate.discard)} - - ${lockedString(UIStringsNotTranslate.saveAll)} - + ${input.onSaveToWorkspace ? html` + + ${lockedString(UIStringsNotTranslate.saveToWorkspace)} + + ` : nothing} `; @@ -377,7 +381,7 @@ export class PatchWidget extends UI.Widget.Widget { this.#applyPatchAbortController?.abort(); }, onDiscard: this.#onDiscard.bind(this), - onSaveAll: this.#onSaveAll.bind(this), + onSaveToWorkspace: this.#canSaveToWorkspace() ? this.#onSaveToWorkspace.bind(this) : undefined, }, this.#viewOutput, this.contentElement); } @@ -487,10 +491,23 @@ ${processedFiles.map(filename => `* ${filename}`).join('\n')}`; this.requestUpdate(); } - #onSaveAll(): void { - // TODO: What should we do for the inspector stylesheet? + #canSaveToWorkspace(): boolean { + if (this.#patchSuggestionState !== PatchSuggestionState.SUCCESS) { + return false; + } + // TODO(crbug.com/406699819): investigate why the inspector-stylesheet shows up here + const filteredModifiedUISourceCodes = + this.#workspaceDiff.modifiedUISourceCodes().filter(sourceCode => sourceCode.origin() !== 'inspector://'); + return filteredModifiedUISourceCodes.length > 0 && + filteredModifiedUISourceCodes.every(sourceCode => this.#persistence.binding(sourceCode)); + } + + #onSaveToWorkspace(): void { this.#workspaceDiff.modifiedUISourceCodes().forEach(modifiedUISourceCode => { - modifiedUISourceCode.commitWorkingCopy(); + const binding = this.#persistence.binding(modifiedUISourceCode); + if (binding) { + binding.fileSystem.commitWorkingCopy(); + } }); this.#savedToDisk = true; diff --git a/front_end/ui/visual_logging/KnownContextValues.ts b/front_end/ui/visual_logging/KnownContextValues.ts index 0c1179085b..9dc831b891 100644 --- a/front_end/ui/visual_logging/KnownContextValues.ts +++ b/front_end/ui/visual_logging/KnownContextValues.ts @@ -2633,7 +2633,7 @@ export const knownContextValues = new Set([ 'passive', 'paste', 'patch-widget.discard', - 'patch-widget.save-all', + 'patch-widget.save-to-workspace', 'path', 'pattern', 'pause',