Skip to content

Commit 99b3858

Browse files
committed
fix(beautify): skip beautify-on-save for file types with no provider
_beautifyOnSave only checked the global on/off preference before running the beautify command, not whether any provider was actually registered for the saved file's language. Saving a file type with no beautify provider (eg. plain text) unconditionally attempted to beautify, which failed and surfaced an error popover on every save. Guard with the same _getEnabledProviders() check already used to enable/disable the manual Beautify command, so unsupported file types silently no-op on save instead. Fixes #3103 Also adds a safety-net afterEach in BeautificationManager-test.js that resets "beautify on save" if a test leaves it toggled on, and a new integration suite (Beautify-integ-test.js) that exercises the real save -> documentSaved -> beautify-on-save flow end to end, covering both the no-provider no-op case and the happy path.
1 parent c987ffd commit 99b3858

4 files changed

Lines changed: 151 additions & 0 deletions

File tree

src/features/BeautificationManager.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,10 @@ define(function (require, exports, module) {
323323
if(!_isBeautifyOnSaveEnabled() || !editor || editor.document.file.fullPath !== doc.file.fullPath){
324324
return;
325325
}
326+
if(!_getEnabledProviders(doc.file.fullPath).length){
327+
// no beautify provider registered for this file type, silently skip instead of showing an error.
328+
return;
329+
}
326330
editor.clearSelection();
327331
_beautifyCommand();
328332
}

test/UnitTestSuite.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ define(function (require, exports, module) {
110110
require("spec/QuickViewManager-test");
111111
require("spec/SelectionViewManager-test");
112112
require("spec/BeautificationManager-test");
113+
require("spec/Beautify-integ-test");
113114
require("spec/Template-for-integ-test");
114115
require("spec/LiveDevelopmentMultiBrowser-test");
115116
require("spec/LiveDevelopmentCustomServer-test");

test/spec/BeautificationManager-test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ define(function (require, exports, module) {
7474

7575
afterEach(async function () {
7676
SpecRunnerUtils.destroyMockEditor(testDocument);
77+
// safety net: a test that fails/times out partway through can leave "beautify on save"
78+
// toggled on, which would otherwise leak into and break unrelated tests/suites.
79+
let beautifyOnSaveCmd = CommandManager.get(Commands.EDIT_BEAUTIFY_CODE_ON_SAVE);
80+
if(beautifyOnSaveCmd.getChecked()){
81+
await awaitsForDone(CommandManager.execute(Commands.EDIT_BEAUTIFY_CODE_ON_SAVE), "beautify on save reset");
82+
}
7783
});
7884

7985
it("should register and unregister beautifier for all languages and beautifyText", async function () {

test/spec/Beautify-integ-test.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* GNU AGPL-3.0 License
3+
*
4+
* Copyright (c) 2021 - present core.ai . All rights reserved.
5+
*
6+
* This program is free software: you can redistribute it and/or modify it
7+
* under the terms of the GNU Affero General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
14+
* for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
18+
*
19+
*/
20+
21+
/*global describe, it, expect, beforeAll, afterAll, afterEach, awaitsFor, awaitsForDone, jsPromise */
22+
23+
define(function (require, exports, module) {
24+
25+
26+
const SpecRunnerUtils = require("spec/SpecRunnerUtils"),
27+
Commands = require("command/Commands");
28+
29+
describe("integration: Beautify", function () {
30+
let testWindow, $, brackets,
31+
CommandManager, FileViewController, EditorManager, FileSystem,
32+
BeautificationManager, testProjectPath;
33+
34+
// "csharp" is a real registered language with no beautify provider shipped in core/default
35+
// extensions - see BeautificationManager-test.js for the same assumption.
36+
const provider = {
37+
beautifyTextProvider: function (textToBeautify) {
38+
return Promise.resolve({originalText: textToBeautify, changedText: "beautified!"});
39+
},
40+
beautifyEditorProvider: function (editor) {
41+
return Promise.resolve({originalText: editor.document.getText(), changedText: "beautified!"});
42+
}
43+
};
44+
45+
beforeAll(async function () {
46+
// do not use force option in brackets core integration tests. Tests are assumed to reuse the existing
47+
// test window instance for fast runs.
48+
testWindow = await SpecRunnerUtils.createTestWindowAndRun();
49+
brackets = testWindow.brackets;
50+
$ = testWindow.$;
51+
CommandManager = brackets.test.CommandManager;
52+
FileViewController = brackets.test.FileViewController;
53+
EditorManager = brackets.test.EditorManager;
54+
FileSystem = brackets.test.FileSystem;
55+
BeautificationManager = brackets.test.BeautificationManager;
56+
57+
testProjectPath = SpecRunnerUtils.getTempDirectory() + "/beautify-test";
58+
await SpecRunnerUtils.createTempDirectory();
59+
await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath);
60+
await jsPromise(SpecRunnerUtils.createTextFile(
61+
testProjectPath + "/plain.txt", "hello world", FileSystem));
62+
await jsPromise(SpecRunnerUtils.createTextFile(
63+
testProjectPath + "/sample.cs", "original code", FileSystem));
64+
65+
await SpecRunnerUtils.loadProjectInTestWindow(testProjectPath);
66+
BeautificationManager.registerBeautificationProvider(provider, ["csharp"]);
67+
}, 30000);
68+
69+
afterAll(async function () {
70+
// safety net: if the test that registers this provider fails/times out before reaching its
71+
// own cleanup, or never runs at all, don't leak the registration past this suite.
72+
try {
73+
BeautificationManager.removeBeautificationProvider(provider, ["csharp"]);
74+
} catch (e) {
75+
// provider was never registered - nothing to clean up.
76+
}
77+
testWindow = null;
78+
$ = null;
79+
brackets = null;
80+
CommandManager = null;
81+
FileViewController = null;
82+
EditorManager = null;
83+
FileSystem = null;
84+
BeautificationManager = null;
85+
await SpecRunnerUtils.closeTestWindow();
86+
}, 30000);
87+
88+
afterEach(async function () {
89+
testWindow.brackets.test.MainViewManager._closeAll(testWindow.brackets.test.MainViewManager.ALL_PANES);
90+
// safety net: a failing test could otherwise leave "beautify on save" toggled on and leak
91+
// into other tests/suites.
92+
let beautifyOnSaveCmd = CommandManager.get(Commands.EDIT_BEAUTIFY_CODE_ON_SAVE);
93+
if(beautifyOnSaveCmd.getChecked()){
94+
await awaitsForDone(CommandManager.execute(Commands.EDIT_BEAUTIFY_CODE_ON_SAVE), "beautify on save reset");
95+
}
96+
});
97+
98+
it("should not show an error popup saving a file type with no beautify provider (#3103)", async function () {
99+
// regression test: https://github.com/phcode-dev/phoenix/issues/3103
100+
// Saving a file whose type has no registered beautification provider (eg. plain text) with
101+
// "beautify on save" enabled should silently be a no-op instead of showing an error popover.
102+
await awaitsForDone(CommandManager.execute(Commands.EDIT_BEAUTIFY_CODE_ON_SAVE), "enable beautify on save");
103+
await awaitsForDone(FileViewController.openAndSelectDocument(
104+
testProjectPath + "/plain.txt", FileViewController.PROJECT_MANAGER), "open plain.txt");
105+
106+
let editor = EditorManager.getActiveEditor();
107+
editor.document.replaceRange(" edited", editor.getEndingCursorPos());
108+
await awaitsForDone(CommandManager.execute(Commands.FILE_SAVE), "save plain.txt");
109+
110+
// A regression here would only show up asynchronously (beautifyEditor's provider lookup
111+
// rejects a couple of promise ticks after "documentSaved" fires), so drain the microtask
112+
// queue a generous number of times before asserting nothing happened. This is a fixed
113+
// number of microtask hops, not a wall-clock wait, so it can't be flaky.
114+
for (let i = 0; i < 20; i++) {
115+
await Promise.resolve();
116+
}
117+
118+
expect($(".popover-message").length).toBe(0);
119+
expect(editor.document.getText()).toBe("hello world edited");
120+
});
121+
122+
it("should beautify on save when a provider is registered for the file type", async function () {
123+
await awaitsForDone(CommandManager.execute(Commands.EDIT_BEAUTIFY_CODE_ON_SAVE), "enable beautify on save");
124+
await awaitsForDone(FileViewController.openAndSelectDocument(
125+
testProjectPath + "/sample.cs", FileViewController.PROJECT_MANAGER), "open sample.cs");
126+
127+
let editor = EditorManager.getActiveEditor();
128+
expect(editor.document.getText()).toBe("original code");
129+
// FILE_SAVE (and thus the "documentSaved" event beautify-on-save listens for) is a no-op
130+
// unless the document is dirty, so make an edit first.
131+
editor.document.replaceRange(" edited", editor.getEndingCursorPos());
132+
await awaitsForDone(CommandManager.execute(Commands.FILE_SAVE), "save sample.cs");
133+
134+
await awaitsFor(function () {
135+
return editor.document.getText() === "beautified!";
136+
}, "waiting for beautify on save done", 5000);
137+
expect(editor.document.getText()).toBe("beautified!");
138+
});
139+
});
140+
});

0 commit comments

Comments
 (0)