|
| 1 | +import { expect } from 'chai'; |
| 2 | +import * as sinon from 'sinon'; |
| 3 | +import * as fsExtra from 'fs-extra'; |
| 4 | +import { standardizePath as s } from 'brighterscript'; |
| 5 | +import { vscode } from './mockVscode.spec'; |
| 6 | +import type { FormattingOptions } from 'brighterscript-formatter'; |
| 7 | +import { Formatter as BrighterScriptFormatter } from 'brighterscript-formatter'; |
| 8 | + |
| 9 | +let Module = require('module'); |
| 10 | + |
| 11 | +//override the "require" call to mock certain items |
| 12 | +const { require: oldRequire } = Module.prototype; |
| 13 | + |
| 14 | +Module.prototype.require = function hijacked(file) { |
| 15 | + if (file === 'vscode') { |
| 16 | + return vscode; |
| 17 | + } else { |
| 18 | + return oldRequire.apply(this, arguments); |
| 19 | + } |
| 20 | +}; |
| 21 | + |
| 22 | +import { Formatter } from './formatter'; |
| 23 | + |
| 24 | +describe('Formatter', () => { |
| 25 | + let sandbox: sinon.SinonSandbox; |
| 26 | + let tempDir: string; |
| 27 | + let formatter: Formatter; |
| 28 | + let customFormattingOptions: FormattingOptions = { |
| 29 | + keywordCase: 'upper', |
| 30 | + formatIndent: true |
| 31 | + }; |
| 32 | + |
| 33 | + beforeEach(() => { |
| 34 | + sandbox = sinon.createSandbox(); |
| 35 | + tempDir = s`${process.cwd()}/.tmp/formatter-tests`; |
| 36 | + fsExtra.ensureDirSync(tempDir); |
| 37 | + formatter = new Formatter(); |
| 38 | + }); |
| 39 | + |
| 40 | + afterEach(() => { |
| 41 | + sandbox.restore(); |
| 42 | + if (fsExtra.pathExistsSync(tempDir)) { |
| 43 | + fsExtra.removeSync(tempDir); |
| 44 | + } |
| 45 | + // Clean up vscode workspace state |
| 46 | + vscode.workspace.workspaceFolders = []; |
| 47 | + vscode.workspace._configuration = {}; |
| 48 | + }); |
| 49 | + |
| 50 | + /** |
| 51 | + * Helper to create a mock document |
| 52 | + */ |
| 53 | + function createMockDocument(uri: string, text: string) { |
| 54 | + const lines = text.split(/\r?\n/); |
| 55 | + return { |
| 56 | + uri: vscode.Uri.file(uri), |
| 57 | + getText: () => text, |
| 58 | + lineAt: (lineNumber: number) => ({ |
| 59 | + text: lines[lineNumber] || '' |
| 60 | + }), |
| 61 | + validateRange: (range) => range |
| 62 | + }; |
| 63 | + } |
| 64 | + |
| 65 | + /** |
| 66 | + * Helper to create a mock range |
| 67 | + */ |
| 68 | + function createMockRange(startLine: number, endLine: number) { |
| 69 | + return { |
| 70 | + start: { line: startLine, character: 0 }, |
| 71 | + end: { line: endLine, character: 0 } |
| 72 | + } as any; |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * Helper to create formatting options |
| 77 | + */ |
| 78 | + function createMockFormattingOptions() { |
| 79 | + return { |
| 80 | + tabSize: 4, |
| 81 | + insertSpaces: true |
| 82 | + }; |
| 83 | + } |
| 84 | + |
| 85 | + /** |
| 86 | + * Helper to test formatter with various configurations |
| 87 | + */ |
| 88 | + async function doTest(options: { |
| 89 | + /** Name for the workspace folder */ |
| 90 | + workspaceName: string; |
| 91 | + /** Optional config files to create. Key is path relative to workspace, value is the config content */ |
| 92 | + configFiles?: Record<string, any>; |
| 93 | + /** VS Code workspace configuration settings */ |
| 94 | + vscodeConfig?: Record<string, any>; |
| 95 | + /** Source text to format */ |
| 96 | + sourceText: string; |
| 97 | + /** Expected formatting options that should be applied */ |
| 98 | + expectedFormattingOptions: FormattingOptions; |
| 99 | + /** Optional custom verification function */ |
| 100 | + customVerify?: (edits: any[]) => void; |
| 101 | + }) { |
| 102 | + const workspaceFolder = s`${tempDir}/${options.workspaceName}`; |
| 103 | + fsExtra.ensureDirSync(workspaceFolder); |
| 104 | + |
| 105 | + // Create any config files |
| 106 | + if (options.configFiles) { |
| 107 | + for (const [relativePath, content] of Object.entries(options.configFiles)) { |
| 108 | + const fullPath = s`${workspaceFolder}/${relativePath}`; |
| 109 | + fsExtra.ensureDirSync(s`${fullPath}/..`); |
| 110 | + fsExtra.writeJsonSync(fullPath, content); |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + // Set up workspace |
| 115 | + vscode.workspace.workspaceFolders = [{ |
| 116 | + uri: vscode.Uri.file(workspaceFolder), |
| 117 | + name: options.workspaceName, |
| 118 | + index: 0 |
| 119 | + }]; |
| 120 | + |
| 121 | + sandbox.stub(vscode.workspace, 'getWorkspaceFolder').returns(vscode.workspace.workspaceFolders[0]); |
| 122 | + |
| 123 | + // Apply VS Code configuration |
| 124 | + vscode.workspace._configuration = options.vscodeConfig || {}; |
| 125 | + |
| 126 | + // Create document and format |
| 127 | + const document = createMockDocument( |
| 128 | + s`${workspaceFolder}/source/main.brs`, |
| 129 | + options.sourceText |
| 130 | + ); |
| 131 | + const lineCount = options.sourceText.split('\n').length; |
| 132 | + const range = createMockRange(0, lineCount - 1); |
| 133 | + const formattingOptions = createMockFormattingOptions(); |
| 134 | + |
| 135 | + const edits = await formatter.provideDocumentRangeFormattingEdits(document as any, range, formattingOptions as any); |
| 136 | + |
| 137 | + // Custom verification or default verification |
| 138 | + if (options.customVerify) { |
| 139 | + options.customVerify(edits); |
| 140 | + } else { |
| 141 | + expect(edits).to.exist; |
| 142 | + expect(edits.length).to.be.greaterThan(0); |
| 143 | + |
| 144 | + // Format with expected settings and compare |
| 145 | + const bsFormatter = new BrighterScriptFormatter(); |
| 146 | + const expectedFormatted = bsFormatter.format(options.sourceText, options.expectedFormattingOptions); |
| 147 | + const actualFormatted = edits.map(e => e.newText).join('\n'); |
| 148 | + expect(actualFormatted).to.equal(expectedFormatted); |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + describe('settings resolution', () => { |
| 153 | + it('uses default settings when no config file found', async () => { |
| 154 | + await doTest({ |
| 155 | + workspaceName: 'workspace1', |
| 156 | + vscodeConfig: { |
| 157 | + 'brightscript.format.keywordCase': 'lower', |
| 158 | + 'brightscript.format.formatIndent': true |
| 159 | + }, |
| 160 | + sourceText: 'SUB Main()\nPRINT "hello"\nEND SUB', |
| 161 | + expectedFormattingOptions: {} |
| 162 | + }); |
| 163 | + }); |
| 164 | + |
| 165 | + it('uses bsfmt.json found at root folder', async () => { |
| 166 | + await doTest({ |
| 167 | + workspaceName: 'workspace2', |
| 168 | + configFiles: { |
| 169 | + 'bsfmt.json': customFormattingOptions |
| 170 | + }, |
| 171 | + vscodeConfig: { |
| 172 | + 'brightscript.format.keywordCase': 'lower' |
| 173 | + }, |
| 174 | + sourceText: 'sub main()\nprint "hello"\nend sub', |
| 175 | + expectedFormattingOptions: { ...customFormattingOptions } |
| 176 | + }); |
| 177 | + }); |
| 178 | + |
| 179 | + it('reads brightscript.format.bsfmtPath from workspace settings', async () => { |
| 180 | + const customConfigFolder = s`${tempDir}/custom-config`; |
| 181 | + fsExtra.ensureDirSync(customConfigFolder); |
| 182 | + |
| 183 | + // Create a custom config file in a different location |
| 184 | + const customBsfmtPath = s`${customConfigFolder}/my-custom-bsfmt.json`; |
| 185 | + fsExtra.writeJsonSync(customBsfmtPath, customFormattingOptions); |
| 186 | + |
| 187 | + await doTest({ |
| 188 | + workspaceName: 'workspace3', |
| 189 | + vscodeConfig: { |
| 190 | + 'brightscript.format.bsfmtPath': customBsfmtPath, |
| 191 | + 'brightscript.format.keywordCase': 'lower' |
| 192 | + }, |
| 193 | + sourceText: 'sub main()\nprint "hello"\nend sub', |
| 194 | + expectedFormattingOptions: { ...customFormattingOptions } |
| 195 | + }); |
| 196 | + }); |
| 197 | + |
| 198 | + it('reads brightscript.format.bsfmtPath as relative path from workspace', async () => { |
| 199 | + await doTest({ |
| 200 | + workspaceName: 'workspace4', |
| 201 | + configFiles: { |
| 202 | + '.vscode/custom-bsfmt.json': customFormattingOptions |
| 203 | + }, |
| 204 | + vscodeConfig: { |
| 205 | + 'brightscript.format.bsfmtPath': '.vscode/custom-bsfmt.json' |
| 206 | + }, |
| 207 | + sourceText: 'sub main()\nelse if true\nend if\nend sub', |
| 208 | + expectedFormattingOptions: { ...customFormattingOptions } |
| 209 | + }); |
| 210 | + }); |
| 211 | + |
| 212 | + it('prioritizes bsfmtPath setting over default bsfmt.json in workspace', async () => { |
| 213 | + const defaultConfig: FormattingOptions = { |
| 214 | + keywordCase: 'lower' |
| 215 | + }; |
| 216 | + |
| 217 | + await doTest({ |
| 218 | + workspaceName: 'workspace5', |
| 219 | + configFiles: { |
| 220 | + 'bsfmt.json': defaultConfig, |
| 221 | + 'config/custom-bsfmt.json': customFormattingOptions |
| 222 | + }, |
| 223 | + vscodeConfig: { |
| 224 | + 'brightscript.format.bsfmtPath': 'config/custom-bsfmt.json' |
| 225 | + }, |
| 226 | + sourceText: 'sub main()\nend sub', |
| 227 | + expectedFormattingOptions: { ...customFormattingOptions } |
| 228 | + }); |
| 229 | + }); |
| 230 | + |
| 231 | + it('handles error when custom bsfmtPath does not exist', async () => { |
| 232 | + const errorStub = sandbox.stub(vscode.window, 'showErrorMessage'); |
| 233 | + |
| 234 | + await doTest({ |
| 235 | + workspaceName: 'workspace6', |
| 236 | + vscodeConfig: { |
| 237 | + 'brightscript.format.bsfmtPath': 'non-existent-config.json' |
| 238 | + }, |
| 239 | + sourceText: 'sub main()\nend sub', |
| 240 | + expectedFormattingOptions: {}, |
| 241 | + customVerify: () => { |
| 242 | + // The getBsfmtOptions throws an error when custom bsfmtPath doesn't exist |
| 243 | + // This gets caught in the try-catch block and shown to the user |
| 244 | + expect(errorStub.called).to.be.true; |
| 245 | + expect(errorStub.firstCall.args[0]).to.include('does not exist'); |
| 246 | + } |
| 247 | + }); |
| 248 | + }); |
| 249 | + }); |
| 250 | +}); |
0 commit comments