Skip to content

Commit

Permalink
added path based auto completion for jest module methods (jest-commun…
Browse files Browse the repository at this point in the history
…ity#963)

* added path based auto completion for jest module methods

* fix test error on windows
  • Loading branch information
connectdotz authored Dec 14, 2022
1 parent 0c2e21b commit 3b5c02e
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 10 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,13 @@ Content
* Starts Jest automatically for most projects with runnable jest configurations.
* Fully integrated with the vscode TestExplorer.
* Supports both automatic and manual test runs at any level, and easy-switch via UI.
* Show individual fail / passes as well as the whole test suites.
* Supports additional IntelliSense for jest methods.
* Show fails inline of the `expect` function, as well as in the problem inspector.
* View and update snapshots interactively.
* Help debug jest tests in vscode.
* Show coverage information in files being tested.
* Track and shows overall workspace/project test stats
* Supports monorepo, react, react-native, vue and various configurations/platforms.
* active community support.

## Installation

Expand Down
4 changes: 3 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as vscode from 'vscode';
import { statusBar } from './StatusBar';
import { ExtensionManager } from './extensionManager';
import { tiContextManager } from './test-provider/test-item-context-manager';
import * as languageProvider from './language-provider';

let extensionManager: ExtensionManager;

Expand All @@ -21,7 +22,8 @@ const addSubscriptions = (context: vscode.ExtensionContext): void => {
...statusBar.register((folder: string) => extensionManager.getByName(folder)),
...extensionManager.register(),
vscode.languages.registerCodeLensProvider(languages, extensionManager.coverageCodeLensProvider),
...tiContextManager.registerCommands()
...tiContextManager.registerCommands(),
...languageProvider.register()
);
};

Expand Down
71 changes: 71 additions & 0 deletions src/language-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as vscode from 'vscode';
import * as path from 'path';

const JestMockModuleApi =
/jest\.(mock|unmock|domock|dontmock|setmock|requireActual|requireMock|createMockFromModule|)\(['"](.*)/gi;
const ImportFileRegex = /^([^\\.].*)\.(json|jsx|tsx|mjs|cjs|js|ts)$/gi;

const toCompletionItem = (
label: string,
kind = vscode.CompletionItemKind.File,
detail?: string
): vscode.CompletionItem => {
const cItem = new vscode.CompletionItem(label, kind);
cItem.detail = detail ?? label;
return cItem;
};

/**
* auto complete path-based parameter for jest module-related methods
*/
export class LocalFileCompletionItemProvider
implements vscode.CompletionItemProvider<vscode.CompletionItem>
{
public async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position
): Promise<vscode.CompletionItem[] | null | undefined> {
const linePrefix = document.lineAt(position).text.slice(0, position.character);
const matched = [...linePrefix.matchAll(JestMockModuleApi)][0];
if (!matched) {
return undefined;
}
const userInput: string = Array.from(matched)[2];
const documentDir = path.dirname(document.uri.fsPath);
const targetDir = path.resolve(documentDir, userInput);

const results = await vscode.workspace.fs.readDirectory(vscode.Uri.file(targetDir));

const items: vscode.CompletionItem[] = [];
results.forEach(([p, fType]) => {
if (fType === vscode.FileType.Directory) {
items.push(toCompletionItem(p, vscode.CompletionItemKind.Folder));
} else if (fType === vscode.FileType.File) {
const matched = [...p.matchAll(ImportFileRegex)][0];
if (matched) {
const [, module, ext] = matched;
if (ext === 'json') {
items.push(toCompletionItem(p));
} else {
items.push(toCompletionItem(module, vscode.CompletionItemKind.File, p));
}
}
}
});
return items;
}
}

export function register(): vscode.Disposable[] {
const selector = [
{ scheme: 'file', language: 'typescript' },
{ scheme: 'file', language: 'javascript' },
];
return [
vscode.languages.registerCompletionItemProvider(
selector,
new LocalFileCompletionItemProvider(),
'/'
),
];
}
23 changes: 16 additions & 7 deletions tests/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ const statusBar = {
};
jest.mock('../src/StatusBar', () => ({ statusBar }));

const languageProvider = {
register: jest.fn(() => []),
};
jest.mock('../src/language-provider', () => languageProvider);

jest.mock('../src/Coverage', () => ({
registerCoverageCodeLens: jest.fn().mockReturnValue([]),
CoverageCodeLensProvider: jest.fn().mockReturnValue({}),
Expand All @@ -26,19 +31,19 @@ const extensionManager = {
};

// tslint:disable-next-line: variable-name
const ExtensionManager = jest.fn();

jest.mock('../src/extensionManager', () => ({
ExtensionManager,
const mockExtensionManager = {
ExtensionManager: jest.fn(() => extensionManager),
getExtensionWindowSettings: jest.fn(() => ({})),
}));
};

jest.mock('../src/extensionManager', () => mockExtensionManager);

import { activate, deactivate } from '../src/extension';

describe('Extension', () => {
beforeEach(() => {
jest.clearAllMocks();
ExtensionManager.mockImplementation(() => extensionManager);
// ExtensionManager.mockImplementation(() => extensionManager);
});
describe('activate()', () => {
const context: any = {
Expand All @@ -53,14 +58,18 @@ describe('Extension', () => {

it('should instantiate ExtensionManager', () => {
activate(context);
expect(ExtensionManager).toHaveBeenCalledTimes(1);
expect(mockExtensionManager.ExtensionManager).toHaveBeenCalledTimes(1);
});

it('should register statusBar', () => {
statusBar.register.mockClear();
activate(context);
expect(statusBar.register).toHaveBeenCalled();
});
it('should register language provider', () => {
activate(context);
expect(languageProvider.register).toHaveBeenCalledTimes(1);
});
});

describe('deactivate()', () => {
Expand Down
121 changes: 121 additions & 0 deletions tests/language-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
jest.unmock('../src/language-provider');

const vscodeMock = {
FileType: {
Unknown: 0,
File: 1,
Directory: 2,
SymbolicLink: 64,
},
CompletionItemKind: {
File: 0,
Folder: 1,
},
CompletionItem: jest.fn().mockImplementation((label, kind) => ({ label, kind })),
workspace: {
fs: {
readDirectory: jest.fn(),
},
},
Uri: {
file: jest.fn(),
},
languages: {
registerCompletionItemProvider: jest.fn(),
},
};
jest.mock('vscode', () => vscodeMock);

import { LocalFileCompletionItemProvider, register } from '../src/language-provider';
import * as vscode from 'vscode';
import * as path from 'path';

describe('LocalFileCompletionItemProvider', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('can auto complete for jest module-path methods', () => {
it.each`
jestMethod | isValid
${'jest.mock'} | ${true}
${'jest.unmock'} | ${true}
${'jest.doMock'} | ${true}
${'jest.dontMock'} | ${true}
${'jest.setMock'} | ${true}
${'jest.requireActual'} | ${true}
${'jest.requireMock'} | ${true}
${'jest.createMockFromModule'} | ${true}
${'jest.spyOn'} | ${false}
${'jest.fn'} | ${false}
`('for $jestMethod', async ({ jestMethod, isValid }) => {
const dirContent = [['file.ts', vscode.FileType.File]];
vscodeMock.workspace.fs.readDirectory.mockResolvedValue(dirContent);
const srcPath = path.join(path.sep, 'project-root', 'src');
const docPath = path.join(srcPath, '__tests__', 'app.test.ts');
const text = `${jestMethod}('../`;
const doc: any = {
uri: { fsPath: docPath },
lineAt: () => ({ text }),
};
const pos: any = { character: text.length };
const provider = new LocalFileCompletionItemProvider();
const items = await provider.provideCompletionItems(doc, pos);
if (!isValid) {
expect(items).toBeUndefined();
} else {
expect(items).not.toBeUndefined();
// resolve relative directory correctly
expect(vscodeMock.Uri.file).toHaveBeenCalledTimes(1);
const targetFolder = (vscodeMock.Uri.file as jest.Mocked<any>).mock.calls[0][0];
expect(targetFolder.endsWith(srcPath)).toBeTruthy();
}
});
});
describe('will only return the vallid file/directories completion items', () => {
it.each`
case | fileInfo | completionItem
${1} | ${['file.ts', vscode.FileType.File]} | ${{ label: 'file', kind: vscode.CompletionItemKind.File }}
${2} | ${['file.tsx', vscode.FileType.File]} | ${{ label: 'file', kind: vscode.CompletionItemKind.File }}
${3} | ${['file.jsx', vscode.FileType.File]} | ${{ label: 'file', kind: vscode.CompletionItemKind.File }}
${4} | ${['file.js', vscode.FileType.File]} | ${{ label: 'file', kind: vscode.CompletionItemKind.File }}
${5} | ${['file.json', vscode.FileType.File]} | ${{ label: 'file.json', kind: vscode.CompletionItemKind.File }}
${6} | ${['.config.json', vscode.FileType.File]} | ${undefined}
${7} | ${['file.js.save', vscode.FileType.File]} | ${undefined}
${8} | ${['dir', vscode.FileType.Directory]} | ${{ label: 'dir', kind: vscode.CompletionItemKind.Folder }}
${9} | ${['file.mjs', vscode.FileType.File]} | ${{ label: 'file', kind: vscode.CompletionItemKind.File }}
${10} | ${['file.cjs', vscode.FileType.File]} | ${{ label: 'file', kind: vscode.CompletionItemKind.File }}
${11} | ${['image.jpg', vscode.FileType.File]} | ${undefined}
${12} | ${['webpack.config', vscode.FileType.File]} | ${undefined}
`('case $case', async ({ fileInfo, completionItem }) => {
vscodeMock.workspace.fs.readDirectory.mockResolvedValue([fileInfo]);
const srcPath = path.join(path.sep, 'project-root', 'src');
const docPath = path.join(srcPath, '__tests__', 'app.test.ts');
const text = `jest.mock('../`;
const doc: any = {
uri: { fsPath: docPath },
lineAt: () => ({ text }),
};
const pos: any = { character: text.length };
const provider = new LocalFileCompletionItemProvider();
const items = await provider.provideCompletionItems(doc, pos);
if (!completionItem) {
expect(items).toEqual([]);
} else {
expect(items[0]).toEqual(expect.objectContaining(completionItem));
}
});
});
});
describe('register', () => {
it('will register the language provider', () => {
register();
expect(vscodeMock.languages.registerCompletionItemProvider).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({ language: 'javascript' }),
expect.objectContaining({ language: 'typescript' }),
]),
expect.any(LocalFileCompletionItemProvider),
'/'
);
});
});

0 comments on commit 3b5c02e

Please sign in to comment.