Skip to content

Commit 237a3e3

Browse files
committed
Add code action tests
1 parent 33268c2 commit 237a3e3

File tree

8 files changed

+403
-16
lines changed

8 files changed

+403
-16
lines changed
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import * as path from 'path';
8+
import { describe, beforeAll, beforeEach, afterAll, test, expect, afterEach } from '@jest/globals';
9+
import testAssetWorkspace from './testAssets/testAssetWorkspace';
10+
import {
11+
activateCSharpExtension,
12+
closeAllEditorsAsync,
13+
expectText,
14+
openFileInWorkspaceAsync,
15+
} from './integrationHelpers';
16+
17+
describe(`[${testAssetWorkspace.description}] Test Code Actions`, () => {
18+
beforeAll(async () => {
19+
await activateCSharpExtension();
20+
});
21+
22+
beforeEach(async () => {
23+
const fileName = path.join('src', 'app', 'CodeActions.cs');
24+
await openFileInWorkspaceAsync(fileName);
25+
});
26+
27+
afterAll(async () => {
28+
await testAssetWorkspace.cleanupWorkspace();
29+
});
30+
31+
afterEach(async () => {
32+
await closeAllEditorsAsync();
33+
});
34+
35+
test('Lightbulb displays actions', async () => {
36+
const actions = await getCodeActions(new vscode.Range(0, 0, 0, 12));
37+
expect(actions.length).toBeGreaterThanOrEqual(3);
38+
39+
// Verify we have unresolved code actions.
40+
expect(actions[0].title).toBe('Remove unnecessary usings');
41+
expect(actions[0].kind).toStrictEqual(vscode.CodeActionKind.QuickFix);
42+
expect(actions[0].edit).toBeUndefined();
43+
expect(actions[0].command).toBeUndefined();
44+
45+
expect(actions[1].title).toBe('Fix All: Remove unnecessary usings');
46+
expect(actions[1].kind).toStrictEqual(vscode.CodeActionKind.QuickFix);
47+
expect(actions[1].edit).toBeUndefined();
48+
expect(actions[1].command).toBeDefined();
49+
50+
expect(actions[2].title).toBe('Suppress or configure issues');
51+
expect(actions[2].kind).toStrictEqual(vscode.CodeActionKind.QuickFix);
52+
expect(actions[2].edit).toBeUndefined();
53+
expect(actions[2].command).toBeDefined();
54+
});
55+
56+
test('Remove unnecessary usings applied', async () => {
57+
const actions = await getCodeActions(new vscode.Range(0, 0, 0, 12), 10);
58+
59+
expect(actions[0].title).toBe('Remove unnecessary usings');
60+
expect(actions[0].edit).toBeDefined();
61+
62+
await vscode.workspace.applyEdit(actions[0].edit!);
63+
64+
expectText(vscode.window.activeTextEditor!.document, [
65+
'namespace CodeActionsTests;',
66+
'',
67+
'class CodeActions',
68+
'{',
69+
' static void Do() { Method(); }',
70+
' static void Method()',
71+
' {',
72+
' var x = 1;',
73+
' Do();',
74+
' }',
75+
'}',
76+
]);
77+
});
78+
79+
test('Add accessibility modifiers applied', async () => {
80+
const actions = await getCodeActions(new vscode.Range(6, 16, 6, 19), 10);
81+
82+
expect(actions[0].title).toBe('Add accessibility modifiers');
83+
expect(actions[0].edit).toBeDefined();
84+
85+
await vscode.workspace.applyEdit(actions[0].edit!);
86+
87+
expectText(vscode.window.activeTextEditor!.document, [
88+
'using System;',
89+
'',
90+
'namespace CodeActionsTests;',
91+
'',
92+
'class CodeActions',
93+
'{',
94+
' private static void Do() { Method(); }',
95+
' static void Method()',
96+
' {',
97+
' var x = 1;',
98+
' Do();',
99+
' }',
100+
'}',
101+
]);
102+
});
103+
104+
test('Fix all in document', async () => {
105+
const action = await getSpecificCodeAction(
106+
new vscode.Range(6, 16, 6, 19),
107+
'Fix All: Add accessibility modifiers'
108+
);
109+
110+
expect(action.edit).toBeUndefined();
111+
expect(action.command).toBeDefined();
112+
113+
await invokeQuickPickAction(action, /*quickPickIndex: Document*/ 0);
114+
115+
expectText(vscode.window.activeTextEditor!.document, [
116+
'using System;',
117+
'',
118+
'namespace CodeActionsTests;',
119+
'',
120+
'internal class CodeActions',
121+
'{',
122+
' private static void Do() { Method(); }',
123+
'',
124+
' private static void Method()',
125+
' {',
126+
' var x = 1;',
127+
' Do();',
128+
' }',
129+
'}',
130+
]);
131+
});
132+
133+
test('Fix all in project', async () => {
134+
const action = await getSpecificCodeAction(
135+
new vscode.Range(6, 16, 6, 19),
136+
'Fix All: Add accessibility modifiers'
137+
);
138+
139+
expect(action.edit).toBeUndefined();
140+
expect(action.command).toBeDefined();
141+
142+
await invokeQuickPickAction(action, /*quickPickIndex: Project*/ 1);
143+
144+
expectText(vscode.window.activeTextEditor!.document, [
145+
'using System;',
146+
'',
147+
'namespace CodeActionsTests;',
148+
'',
149+
'internal class CodeActions',
150+
'{',
151+
' private static void Do() { Method(); }',
152+
'',
153+
' private static void Method()',
154+
' {',
155+
' var x = 1;',
156+
' Do();',
157+
' }',
158+
'}',
159+
]);
160+
161+
const projectFile = vscode.workspace.textDocuments.find((d) => d.fileName.endsWith('CodeActionsInProject.cs'));
162+
expect(projectFile).toBeDefined();
163+
expectText(projectFile!, [
164+
'using System;',
165+
'',
166+
'namespace CodeActionsTests;',
167+
'',
168+
'internal class CodeActionsInProject',
169+
'{',
170+
'}',
171+
]);
172+
});
173+
174+
test('Fix all in solution', async () => {
175+
const action = await getSpecificCodeAction(
176+
new vscode.Range(6, 16, 6, 19),
177+
'Fix All: Add accessibility modifiers'
178+
);
179+
180+
expect(action.edit).toBeUndefined();
181+
expect(action.command).toBeDefined();
182+
183+
await invokeQuickPickAction(action, /*quickPickIndex: Solution*/ 2);
184+
185+
expectText(vscode.window.activeTextEditor!.document, [
186+
'using System;',
187+
'',
188+
'namespace CodeActionsTests;',
189+
'',
190+
'internal class CodeActions',
191+
'{',
192+
' private static void Do() { Method(); }',
193+
'',
194+
' private static void Method()',
195+
' {',
196+
' var x = 1;',
197+
' Do();',
198+
' }',
199+
'}',
200+
]);
201+
202+
const currentProjectFile = vscode.workspace.textDocuments.find((d) =>
203+
d.fileName.endsWith('CodeActionsInProject.cs')
204+
);
205+
expect(currentProjectFile).toBeDefined();
206+
expectText(currentProjectFile!, [
207+
'using System;',
208+
'',
209+
'namespace CodeActionsTests;',
210+
'',
211+
'internal class CodeActionsInProject',
212+
'{',
213+
'}',
214+
]);
215+
216+
const otherProjectFile = vscode.workspace.textDocuments.find((d) =>
217+
d.fileName.endsWith('CodeActionsInSolution.cs')
218+
);
219+
expect(otherProjectFile).toBeDefined();
220+
expectText(otherProjectFile!, [
221+
'using System;',
222+
'',
223+
'namespace CodeActionsTests;',
224+
'',
225+
'internal class CodeActionsInSolution',
226+
'{',
227+
'}',
228+
]);
229+
});
230+
231+
test('Nested action', async () => {
232+
const action = await getSpecificCodeAction(new vscode.Range(9, 12, 9, 12), 'Convert number');
233+
234+
expect(action.edit).toBeUndefined();
235+
expect(action.command).toBeDefined();
236+
237+
await invokeQuickPickAction(action, /*quickPickIndex: Convert to binary*/ 0);
238+
239+
expectText(vscode.window.activeTextEditor!.document, [
240+
'using System;',
241+
'',
242+
'namespace CodeActionsTests;',
243+
'',
244+
'class CodeActions',
245+
'{',
246+
' static void Do() { Method(); }',
247+
' static void Method()',
248+
' {',
249+
' var x = 0b1;',
250+
' Do();',
251+
' }',
252+
'}',
253+
]);
254+
});
255+
256+
test('Suppress warning', async () => {
257+
const action = await getSpecificCodeAction(new vscode.Range(9, 12, 9, 12), 'Suppress or configure issues');
258+
259+
expect(action.edit).toBeUndefined();
260+
expect(action.command).toBeDefined();
261+
262+
await invokeQuickPickAction(action, /*quickPickIndex: Suppress CS0219 -> in Source*/ 0);
263+
264+
expectText(vscode.window.activeTextEditor!.document, [
265+
'using System;',
266+
'',
267+
'namespace CodeActionsTests;',
268+
'',
269+
'class CodeActions',
270+
'{',
271+
' static void Do() { Method(); }',
272+
' static void Method()',
273+
' {',
274+
'#pragma warning disable CS0219 // Variable is assigned but its value is never used',
275+
' var x = 1;',
276+
'#pragma warning restore CS0219 // Variable is assigned but its value is never used',
277+
' Do();',
278+
' }',
279+
'}',
280+
]);
281+
});
282+
283+
test('Configure code style option', async () => {
284+
const action = await getSpecificCodeAction(new vscode.Range(6, 16, 6, 19), 'Suppress or configure issues');
285+
286+
expect(action.edit).toBeUndefined();
287+
expect(action.command).toBeDefined();
288+
289+
await invokeQuickPickAction(action, /*quickPickIndex: Configure IDE0040 code style -> never*/ 0);
290+
291+
const editorConfigFile = vscode.workspace.textDocuments.find((d) => d.fileName.endsWith('.editorconfig'));
292+
expect(editorConfigFile).toBeDefined();
293+
expect(editorConfigFile!.getText()).toContain('dotnet_style_require_accessibility_modifiers = never');
294+
});
295+
296+
test('Configure analyzer severity', async () => {
297+
const action = await getSpecificCodeAction(new vscode.Range(6, 16, 6, 19), 'Suppress or configure issues');
298+
299+
expect(action.edit).toBeUndefined();
300+
expect(action.command).toBeDefined();
301+
302+
await invokeQuickPickAction(action, /*quickPickIndex: Configure IDE0040 severity -> None*/ 4);
303+
304+
const editorConfigFile = vscode.workspace.textDocuments.find((d) => d.fileName.endsWith('.editorconfig'));
305+
expect(editorConfigFile).toBeDefined();
306+
expect(editorConfigFile!.getText()).toContain('dotnet_diagnostic.IDE0040.severity = none');
307+
});
308+
});
309+
310+
async function getCodeActions(
311+
range: vscode.Range,
312+
resolveCount: number | undefined = undefined
313+
): Promise<vscode.CodeAction[]> {
314+
const codeActions = await vscode.commands.executeCommand<vscode.CodeAction[]>(
315+
'vscode.executeCodeActionProvider',
316+
vscode.window.activeTextEditor!.document.uri,
317+
range,
318+
/** kind **/ undefined,
319+
resolveCount
320+
);
321+
return codeActions;
322+
}
323+
324+
async function getSpecificCodeAction(range: vscode.Range, title: string): Promise<vscode.CodeAction> {
325+
const codeActions = await getCodeActions(range, 100);
326+
const action = codeActions.find((action) => action.title === title);
327+
if (!action) {
328+
throw new Error(`Code action '${title}' not found in ${codeActions.map((a) => a.title).join(', ')}`);
329+
}
330+
return action;
331+
}
332+
333+
async function invokeQuickPickAction(codeAction: vscode.CodeAction, quickPickIndex: number): Promise<void> {
334+
// Invoke, but do not await the command (the command blocks until a quick pick item is resolved)
335+
const promise = vscode.commands.executeCommand(codeAction.command!.command, ...codeAction.command!.arguments!);
336+
337+
// workbench.action.quickOpenSelectNext selects the next quick pick item.
338+
// It must be called at least once to select the first item.
339+
for (let i = 0; i <= quickPickIndex; i++) {
340+
// First call ensures the quick pick is populated with items and the first is selected.
341+
await vscode.commands.executeCommand('workbench.action.quickOpenSelectNext');
342+
}
343+
344+
// Accept the selected item
345+
await vscode.commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem');
346+
347+
// Finally wait for the command to complete.
348+
await promise;
349+
}

test/lsptoolshost/integrationTests/formatting.integration.test.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@
66
import * as vscode from 'vscode';
77
import * as path from 'path';
88
import testAssetWorkspace from './testAssets/testAssetWorkspace';
9-
import { activateCSharpExtension, closeAllEditorsAsync, openFileInWorkspaceAsync } from './integrationHelpers';
9+
import {
10+
activateCSharpExtension,
11+
closeAllEditorsAsync,
12+
expectText,
13+
openFileInWorkspaceAsync,
14+
} from './integrationHelpers';
1015
import { describe, beforeAll, beforeEach, afterAll, test, afterEach } from '@jest/globals';
11-
import { expectText, formatDocumentAsync, formatOnTypeAsync, formatRangeAsync } from './formattingTestHelpers';
16+
import { formatDocumentAsync, formatOnTypeAsync, formatRangeAsync } from './formattingTestHelpers';
1217

1318
describe(`[${testAssetWorkspace.description}] Formatting Tests`, () => {
1419
beforeAll(async () => {
@@ -45,7 +50,7 @@ describe(`[${testAssetWorkspace.description}] Formatting Tests`, () => {
4550
' }',
4651
'}',
4752
];
48-
expectText(expectedText);
53+
expectText(vscode.window.activeTextEditor!.document, expectedText);
4954
});
5055

5156
test('Document range formatting formats only the range', async () => {
@@ -65,7 +70,7 @@ describe(`[${testAssetWorkspace.description}] Formatting Tests`, () => {
6570
' }',
6671
'}',
6772
];
68-
expectText(expectedText);
73+
expectText(vscode.window.activeTextEditor!.document, expectedText);
6974
});
7075

7176
test('Document on type formatting formats the typed location', async () => {
@@ -84,6 +89,6 @@ describe(`[${testAssetWorkspace.description}] Formatting Tests`, () => {
8489
' }',
8590
'}',
8691
];
87-
expectText(expectedText);
92+
expectText(vscode.window.activeTextEditor!.document, expectedText);
8893
});
8994
});

0 commit comments

Comments
 (0)