Skip to content

Commit 4a53a63

Browse files
authored
Set focus to editor and navigate to tests upon selection in test explorer (#4354)
* Set focus to editor and focus upon selection in tree * More telemetry * Fixed tests
1 parent ea16fd1 commit 4a53a63

12 files changed

+113
-52
lines changed

src/client/telemetry/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,10 @@ export function captureTelemetry<P extends IEventNamePropertyMapping, E extends
115115
failureEventName?: E
116116
) {
117117
// tslint:disable-next-line:no-function-expression no-any
118-
return function(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) {
118+
return function (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) {
119119
const originalMethod = descriptor.value;
120120
// tslint:disable-next-line:no-function-expression no-any
121-
descriptor.value = function(...args: any[]) {
121+
descriptor.value = function (...args: any[]) {
122122
if (!captureDuration) {
123123
sendTelemetryEvent(eventName, undefined, properties);
124124
// tslint:disable-next-line:no-invalid-this
@@ -327,6 +327,6 @@ interface IEventNamePropertyMapping {
327327
[Telemetry.SubmitCellThroughInput]: never | undefined;
328328
[Telemetry.Undo]: never | undefined;
329329
[EventName.UNITTEST_NAVIGATE_TEST_FILE]: never | undefined;
330-
[EventName.UNITTEST_NAVIGATE_TEST_FUNCTION]: never | undefined;
331-
[EventName.UNITTEST_NAVIGATE_TEST_SUITE]: never | undefined;
330+
[EventName.UNITTEST_NAVIGATE_TEST_FUNCTION]: { focus: boolean };
331+
[EventName.UNITTEST_NAVIGATE_TEST_SUITE]: { focus: boolean };
332332
}

src/client/unittests/navigation/fileNavigator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import { ITestCodeNavigator, ITestNavigatorHelper } from './types';
1313

1414
@injectable()
1515
export class TestFileCodeNavigator implements ITestCodeNavigator {
16-
constructor(@inject(ITestNavigatorHelper) private readonly helper: ITestNavigatorHelper) {}
16+
constructor(@inject(ITestNavigatorHelper) private readonly helper: ITestNavigatorHelper) { }
1717
@swallowExceptions('Navigate to test file')
1818
@captureTelemetry(EventName.UNITTEST_NAVIGATE_TEST_FILE, undefined, true)
19-
public async navigateTo(_: Uri, item: TestFile): Promise<void> {
19+
public async navigateTo(_: Uri, item: TestFile, __: boolean): Promise<void> {
2020
await this.helper.openFile(Uri.file(item.fullPath));
2121
}
2222
}

src/client/unittests/navigation/functionNavigator.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55

66
import { inject, injectable } from 'inversify';
77
import { CancellationTokenSource, Range, SymbolInformation, SymbolKind, TextEditorRevealType, Uri } from 'vscode';
8+
import { IDocumentManager } from '../../common/application/types';
89
import { traceError } from '../../common/logger';
910
import { swallowExceptions } from '../../common/utils/decorators';
10-
import { captureTelemetry } from '../../telemetry';
11+
import { captureTelemetry, sendTelemetryEvent } from '../../telemetry';
1112
import { EventName } from '../../telemetry/constants';
1213
import { ITestCollectionStorageService, TestFunction } from '../common/types';
1314
import { ITestCodeNavigator, ITestNavigatorHelper } from './types';
@@ -17,11 +18,13 @@ export class TestFunctionCodeNavigator implements ITestCodeNavigator {
1718
private cancellationToken?: CancellationTokenSource;
1819
constructor(
1920
@inject(ITestNavigatorHelper) private readonly helper: ITestNavigatorHelper,
21+
@inject(IDocumentManager) private readonly docManager: IDocumentManager,
2022
@inject(ITestCollectionStorageService) private readonly storage: ITestCollectionStorageService
21-
) {}
23+
) { }
2224
@swallowExceptions('Navigate to test function')
2325
@captureTelemetry(EventName.UNITTEST_NAVIGATE_TEST_FUNCTION, undefined, true)
24-
public async navigateTo(resource: Uri, fn: TestFunction): Promise<void> {
26+
public async navigateTo(resource: Uri, fn: TestFunction, focus: boolean = true): Promise<void> {
27+
sendTelemetryEvent(EventName.UNITTEST_NAVIGATE_TEST_FUNCTION, undefined, { focus });
2528
if (this.cancellationToken) {
2629
this.cancellationToken.cancel();
2730
}
@@ -33,7 +36,7 @@ export class TestFunctionCodeNavigator implements ITestCodeNavigator {
3336
const [doc, editor] = await this.helper.openFile(Uri.file(item.parentTestFile.fullPath));
3437
let range: Range | undefined;
3538
if (item.testFunction.line) {
36-
range = new Range(item.testFunction.line, 0, item.testFunction.line + 1, 0);
39+
range = new Range(item.testFunction.line, 0, item.testFunction.line, 0);
3740
} else {
3841
const predicate = (s: SymbolInformation) => s.name === item.testFunction.name && (s.kind === SymbolKind.Method || s.kind === SymbolKind.Function);
3942
const symbol = await this.helper.findSymbol(doc, predicate, this.cancellationToken.token);
@@ -43,6 +46,11 @@ export class TestFunctionCodeNavigator implements ITestCodeNavigator {
4346
traceError('Unable to navigate to test function', new Error('Test Function not found'));
4447
return;
4548
}
46-
editor.revealRange(range, TextEditorRevealType.Default);
49+
if (focus) {
50+
range = new Range(range.start.line, range.start.character, range.start.line, range.start.character);
51+
await this.docManager.showTextDocument(doc, { preserveFocus: false, selection: range });
52+
} else {
53+
editor.revealRange(range, TextEditorRevealType.Default);
54+
}
4755
}
4856
}

src/client/unittests/navigation/helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class TestNavigatorHelper implements ITestNavigatorHelper {
1515
constructor(
1616
@inject(IDocumentManager) private readonly documentManager: IDocumentManager,
1717
@inject(IDocumentSymbolProvider) @named('test') private readonly symbolProvider: IDocumentSymbolProvider
18-
) {}
18+
) { }
1919
public async openFile(file?: Uri): Promise<[TextDocument, TextEditor]> {
2020
if (!file) {
2121
throw new Error('Unable to navigate to an undefined test file');

src/client/unittests/navigation/suiteNavigator.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55

66
import { inject, injectable } from 'inversify';
77
import { CancellationTokenSource, Range, SymbolInformation, SymbolKind, TextEditorRevealType, Uri } from 'vscode';
8+
import { IDocumentManager } from '../../common/application/types';
89
import { traceError } from '../../common/logger';
910
import { swallowExceptions } from '../../common/utils/decorators';
10-
import { captureTelemetry } from '../../telemetry';
11+
import { captureTelemetry, sendTelemetryEvent } from '../../telemetry';
1112
import { EventName } from '../../telemetry/constants';
1213
import { ITestCollectionStorageService, TestSuite } from '../common/types';
1314
import { ITestCodeNavigator, ITestNavigatorHelper } from './types';
@@ -17,11 +18,13 @@ export class TestSuiteCodeNavigator implements ITestCodeNavigator {
1718
private cancellationToken?: CancellationTokenSource;
1819
constructor(
1920
@inject(ITestNavigatorHelper) private readonly helper: ITestNavigatorHelper,
21+
@inject(IDocumentManager) private readonly docManager: IDocumentManager,
2022
@inject(ITestCollectionStorageService) private readonly storage: ITestCollectionStorageService
21-
) {}
23+
) { }
2224
@swallowExceptions('Navigate to test suite')
2325
@captureTelemetry(EventName.UNITTEST_NAVIGATE_TEST_SUITE, undefined, true)
24-
public async navigateTo(resource: Uri, suite: TestSuite): Promise<void> {
26+
public async navigateTo(resource: Uri, suite: TestSuite, focus: boolean = true): Promise<void> {
27+
sendTelemetryEvent(EventName.UNITTEST_NAVIGATE_TEST_SUITE, undefined, { focus });
2528
if (this.cancellationToken) {
2629
this.cancellationToken.cancel();
2730
}
@@ -33,7 +36,7 @@ export class TestSuiteCodeNavigator implements ITestCodeNavigator {
3336
const [doc, editor] = await this.helper.openFile(Uri.file(item.parentTestFile.fullPath));
3437
let range: Range | undefined;
3538
if (item.testSuite.line) {
36-
range = new Range(item.testSuite.line, 0, item.testSuite.line + 1, 0);
39+
range = new Range(item.testSuite.line, 0, item.testSuite.line, 0);
3740
} else {
3841
const predicate = (s: SymbolInformation) => s.name === item.testSuite.name && s.kind === SymbolKind.Class;
3942
const symbol = await this.helper.findSymbol(doc, predicate, this.cancellationToken.token);
@@ -43,6 +46,11 @@ export class TestSuiteCodeNavigator implements ITestCodeNavigator {
4346
traceError('Unable to navigate to test suite', new Error('Test Suite not found'));
4447
return;
4548
}
46-
editor.revealRange(range, TextEditorRevealType.Default);
49+
if (focus) {
50+
range = new Range(range.start.line, range.start.character, range.start.line, range.start.character);
51+
await this.docManager.showTextDocument(doc, { preserveFocus: false, selection: range });
52+
} else {
53+
editor.revealRange(range, TextEditorRevealType.Default);
54+
}
4755
}
4856
}

src/client/unittests/navigation/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export enum NavigableItemType {
2020

2121
export const ITestCodeNavigator = Symbol('ITestCodeNavigator');
2222
export interface ITestCodeNavigator {
23-
navigateTo(resource: Uri, item: NavigableItem): Promise<void>;
23+
navigateTo(resource: Uri, item: NavigableItem, focus: boolean): Promise<void>;
2424
}
2525

2626
export const ITestNavigatorHelper = Symbol('ITestNavigatorHelper');

src/client/unittests/providers/commandHandlers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class TestExplorerCommandHandler implements ITestExplorerCommandHandler {
5050
throw new Error('Unknown Test Type');
5151
}
5252

53-
this.cmdManager.executeCommand(command, item.resource, item.data);
53+
this.cmdManager.executeCommand(command, item.resource, item.data, true);
5454
}
5555
protected async runDebugTestNode(item: TestTreeItem, runType: 'run' | 'debug'): Promise<void> {
5656
let testToRun: TestsToRun;

src/client/unittests/providers/testTreeViewItem.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import {
77
TreeItem, TreeItemCollapsibleState, Uri
88
} from 'vscode';
9+
import { Commands } from '../../common/constants';
10+
import { noop } from '../../common/utils/misc';
911
import { TestsHelper } from '../common/testUtils';
1012
import {
1113
TestFile, TestFolder, TestFunction,
@@ -43,6 +45,7 @@ export class TestTreeItem extends TreeItem {
4345
this.tooltip = `Status: ${testStatus}`;
4446
this.testType = TestsHelper.getTestType(this.data);
4547
this.contextValue = TestsHelper.getTestType(this.data);
48+
this.setCommand();
4649
}
4750

4851
public static createFromFolder(
@@ -151,4 +154,23 @@ export class TestTreeItem extends TreeItem {
151154
public get parent(): TestTreeItem | undefined {
152155
return this.myParent;
153156
}
157+
private setCommand() {
158+
switch (this.testType) {
159+
case TestType.testFile: {
160+
this.command = { command: Commands.navigateToTestFile, title: 'Open', arguments: [this.resource, this.data] };
161+
break;
162+
}
163+
case TestType.testFunction: {
164+
this.command = { command: Commands.navigateToTestFunction, title: 'Open', arguments: [this.resource, this.data, false] };
165+
break;
166+
}
167+
case TestType.testSuite: {
168+
this.command = { command: Commands.navigateToTestSuite, title: 'Open', arguments: [this.resource, this.data, false] };
169+
break;
170+
}
171+
default: {
172+
noop();
173+
}
174+
}
175+
}
154176
}

src/test/unittests/explorer/testExplorerCommandHandler.unit.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ suite('Unit Tests - Test Explorer Command Hanlder', () => {
6161
const handler = capture(cmdManager.registerCommand).last()[1];
6262
await handler.bind(commandHandler)(instance(treeItem));
6363

64-
verify(cmdManager.executeCommand(expectedCommand, resource, data)).once();
64+
verify(cmdManager.executeCommand(expectedCommand, resource, data, true)).once();
6565
}
6666
test('Opening a file will invoke correct command', async () => {
6767
const testFilePath = 'some file path';

src/test/unittests/navigation/fileNavigator.unit.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ suite('Unit Tests - Navigation File', () => {
2525
const filePath = Uri.file('some file Path');
2626
when(helper.openFile(anything())).thenResolve();
2727

28-
await navigator.navigateTo(filePath, { fullPath: filePath.fsPath } as any);
28+
await navigator.navigateTo(filePath, { fullPath: filePath.fsPath } as any, false);
2929

3030
verify(helper.openFile(anything())).once();
3131
expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath);
@@ -34,7 +34,7 @@ suite('Unit Tests - Navigation File', () => {
3434
const filePath = Uri.file('some file Path');
3535
when(helper.openFile(anything())).thenReject(new Error('kaboom'));
3636

37-
await navigator.navigateTo(filePath, { fullPath: filePath.fsPath } as any);
37+
await navigator.navigateTo(filePath, { fullPath: filePath.fsPath } as any, false);
3838

3939
verify(helper.openFile(anything())).once();
4040
expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath);

src/test/unittests/navigation/functionNavigator.unit.test.ts

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55

66
import { expect, use } from 'chai';
77
import * as chaisAsPromised from 'chai-as-promised';
8-
import { anything, capture, instance, mock, verify, when } from 'ts-mockito';
8+
import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito';
99
import * as typemoq from 'typemoq';
1010
import { Location, Range, SymbolInformation, SymbolKind, TextDocument, TextEditor, TextEditorRevealType, Uri } from 'vscode';
11+
import { DocumentManager } from '../../../client/common/application/documentManager';
12+
import { IDocumentManager } from '../../../client/common/application/types';
1113
import { TestCollectionStorageService } from '../../../client/unittests/common/services/storageService';
1214
import { ITestCollectionStorageService } from '../../../client/unittests/common/types';
1315
import { TestFunctionCodeNavigator } from '../../../client/unittests/navigation/functionNavigator';
@@ -20,15 +22,17 @@ use(chaisAsPromised);
2022
suite('Unit Tests - Navigation Function', () => {
2123
let navigator: TestFunctionCodeNavigator;
2224
let helper: ITestNavigatorHelper;
25+
let docManager: IDocumentManager;
2326
let doc: typemoq.IMock<TextDocument>;
2427
let editor: typemoq.IMock<TextEditor>;
2528
let storage: ITestCollectionStorageService;
2629
setup(() => {
2730
doc = typemoq.Mock.ofType<TextDocument>();
2831
editor = typemoq.Mock.ofType<TextEditor>();
2932
helper = mock(TestNavigatorHelper);
33+
docManager = mock(DocumentManager);
3034
storage = mock(TestCollectionStorageService);
31-
navigator = new TestFunctionCodeNavigator(instance(helper), instance(storage));
35+
navigator = new TestFunctionCodeNavigator(instance(helper), instance(docManager), instance(storage));
3236
});
3337
test('Ensure file is opened', async () => {
3438
const filePath = Uri.file('some file Path');
@@ -52,13 +56,13 @@ suite('Unit Tests - Navigation Function', () => {
5256
verify(helper.openFile(anything())).once();
5357
expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath);
5458
});
55-
test('Ensure we use line number from test function when navigating in file', async () => {
59+
async function navigateToFunction(focusCode: boolean) {
5660
const filePath = Uri.file('some file Path');
5761
const line = 999;
5862
when(helper.openFile(anything())).thenResolve([doc.object, editor.object]);
5963
const flattenedFn = { parentTestFile: { fullPath: filePath.fsPath }, testFunction: { name: 'function_name' } };
6064
when(storage.findFlattendTestFunction(filePath, anything())).thenReturn(flattenedFn as any);
61-
const range = new Range(line, 0, line + 1, 0);
65+
const range = new Range(line, 0, line, 0);
6266
const symbol: SymbolInformation = {
6367
containerName: '',
6468
kind: SymbolKind.Function,
@@ -67,27 +71,22 @@ suite('Unit Tests - Navigation Function', () => {
6771
};
6872
when(helper.findSymbol(doc.object, anything(), anything())).thenResolve(symbol);
6973

70-
await navigator.navigateTo(filePath, { name: 'function_name' } as any);
74+
await navigator.navigateTo(filePath, { name: 'function_name' } as any, focusCode);
7175

7276
verify(helper.openFile(anything())).once();
7377
verify(helper.findSymbol(doc.object, anything(), anything())).once();
7478
expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath);
75-
editor.verify(e => e.revealRange(range, TextEditorRevealType.Default), typemoq.Times.once());
79+
if (focusCode) {
80+
verify(docManager.showTextDocument(doc.object, deepEqual({ preserveFocus: false, selection: range }))).once();
81+
} else {
82+
editor.verify(e => e.revealRange(typemoq.It.isAny(), TextEditorRevealType.Default), typemoq.Times.once());
83+
}
84+
}
85+
test('Ensure we use line number from test function when navigating in file (without focusing code)', async () => {
86+
await navigateToFunction(false);
7687
});
77-
test('Ensure we use line number from test function when navigating in file', async () => {
78-
const filePath = Uri.file('some file Path');
79-
const line = 999;
80-
when(helper.openFile(anything())).thenResolve([doc.object, editor.object]);
81-
const flattenedFn = { parentTestFile: { fullPath: filePath.fsPath }, testFunction: { line } };
82-
when(storage.findFlattendTestFunction(filePath, anything())).thenReturn(flattenedFn as any);
83-
const range = new Range(line, 0, line + 1, 0);
84-
85-
await navigator.navigateTo(filePath, { line } as any);
86-
87-
verify(helper.openFile(anything())).once();
88-
verify(helper.findSymbol(anything(), anything(), anything())).never();
89-
expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath);
90-
editor.verify(e => e.revealRange(range, TextEditorRevealType.Default), typemoq.Times.once());
88+
test('Ensure we use line number from test function when navigating in file (focusing code)', async () => {
89+
await navigateToFunction(true);
9190
});
9291
test('Ensure file is opened and range not revealed', async () => {
9392
const filePath = Uri.file('some file Path');

0 commit comments

Comments
 (0)