Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
},
"activationEvents": [
"workspaceContains:**/.mocharc.{js,cjs,yaml,yml,json,jsonc}",
"workspaceContains:**/package.json",
"onCommand:mocha-vscode.getControllersForTest"
],
"repository": {
Expand Down
46 changes: 46 additions & 0 deletions src/configurationFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export class ConfigurationFile implements vscode.Disposable {
private readonly ds = new DisposableStore();
private readonly didDeleteEmitter = this.ds.add(new vscode.EventEmitter<void>());
private readonly didChangeEmitter = this.ds.add(new vscode.EventEmitter<void>());
private readonly activateEmitter = this.ds.add(new vscode.EventEmitter<void>());

private _activateFired: boolean = false;
private _resolver?: resolveModule.Resolver;
private _optionsModule?: OptionsModule;
private _configModule?: ConfigModule;
Expand All @@ -49,6 +51,12 @@ export class ConfigurationFile implements vscode.Disposable {
/** Fired when the file changes. */
public readonly onDidChange = this.didChangeEmitter.event;

/**
* Fired the config file becomes active for actually handling tests
* (e.g. not fired on package.json without mocha section).
*/
public readonly onActivate = this.activateEmitter.event;

constructor(
private readonly logChannel: vscode.LogOutputChannel,
public readonly uri: vscode.Uri,
Expand All @@ -65,6 +73,7 @@ export class ConfigurationFile implements vscode.Disposable {
changeDebounce = undefined;
this.readPromise = undefined;
this.didChangeEmitter.fire();
this.tryActivate();
}, 300);
}),
);
Expand All @@ -77,6 +86,43 @@ export class ConfigurationFile implements vscode.Disposable {
);
}

public get isActive() {
return this._activateFired;
}

public async tryActivate(): Promise<boolean> {
if (this._activateFired) {
return true;
}

const configFile = path.basename(this.uri.fsPath).toLowerCase();
if (configFile === 'package.json') {
try {
const packageJson = JSON.parse(await fs.promises.readFile(this.uri.fsPath, 'utf-8'));
if ('mocha' in packageJson && typeof packageJson.mocha !== 'undefined') {
this.logChannel.trace('Found mocha section in package.config, skipping activation');
this.activateEmitter.fire();
this._activateFired = true;
return true;
} else {
this.logChannel.trace('No mocha section in package.config, skipping activation');
}
} catch (e) {
this.logChannel.warn(
'Error while reading mocha options from package.config, skipping activation',
e,
);
}
} else {
// for normal mocharc files directly activate
this.activateEmitter.fire();
this._activateFired = true;
return true;
}

return false;
}

/**
* Reads the config file from disk.
* @throws {HumanError} if anything goes wrong
Expand Down
3 changes: 2 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import path from 'path';
import type { IExtensionSettings } from './discoverer/types';

/** Pattern of files the CLI looks for */
export const configFilePattern = '**/.mocharc.{js,cjs,yaml,yml,json,jsonc}';
export const configFilePatterns = ['**/.mocharc.{js,cjs,yaml,yml,json,jsonc}', '**/package.json'];

export const defaultTestSymbols: IExtensionSettings = {
suite: ['describe', 'suite'],
Expand All @@ -23,6 +23,7 @@ export const defaultTestSymbols: IExtensionSettings = {

export const showConfigErrorCommand = 'mocha-vscode.showConfigError';
export const getControllersForTestCommand = 'mocha-vscode.getControllersForTest';
export const recreateControllersForTestCommand = 'mocha-vscode.recreateControllersForTestCommand';

function equalsIgnoreCase(a: string, b: string) {
return a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0;
Expand Down
93 changes: 62 additions & 31 deletions src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class Controller {
*/
private currentConfig?: ConfigurationList;

private discoverer!: SettingsBasedFallbackTestDiscoverer;
private discoverer?: SettingsBasedFallbackTestDiscoverer;

public readonly settings = this.disposables.add(
new ConfigValue('extractSettings', defaultTestSymbols),
Expand Down Expand Up @@ -107,13 +107,17 @@ export class Controller {
public readonly onDidDispose = this.disposeEmitter.event;
private tsconfigStore?: TsConfigStore;

public ctrl: vscode.TestController;
public ctrl?: vscode.TestController;

/** Gets run profiles the controller has registerd. */
public get profiles() {
return [...this.runProfiles.values()].flat();
}

public tryActivate() {
return this.configFile.tryActivate();
}

constructor(
private readonly logChannel: vscode.LogOutputChannel,
private readonly wf: vscode.WorkspaceFolder,
Expand All @@ -126,34 +130,49 @@ export class Controller {
wf.uri.fsPath,
configFileUri.fsPath,
);
const ctrl = (this.ctrl = vscode.tests.createTestController(
configFileUri.toString(),
configFileUri.fsPath,
));
this.disposables.add(ctrl);
this.configFile = this.disposables.add(new ConfigurationFile(logChannel, configFileUri, wf));

this.recreateDiscoverer();
this.disposables.add(
this.configFile.onActivate(() => {
try {
const ctrl = (this.ctrl = vscode.tests.createTestController(
configFileUri.toString(),
configFileUri.fsPath,
));
this.disposables.add(ctrl);

this.recreateDiscoverer();
const rescan = async (reason: string) => {
try {
logChannel.info(`Rescan of tests triggered (${reason}) - ${this.configFile.uri}}`);
this.recreateDiscoverer();
await this.scanFiles();
} catch (e) {
this.logChannel.error(e as Error, 'Failed to rescan tests');
}
};
this.disposables.add(this.configFile.onDidChange(() => rescan('mocharc changed')));
this.disposables.add(this.settings.onDidChange(() => rescan('settings changed')));
ctrl.refreshHandler = () => {
this.configFile.forget();
rescan('user');
};
this.scanFiles();
} catch (e) {
this.logChannel.error(e as Error);
}
}),
);

const rescan = async (reason: string) => {
try {
logChannel.info(`Rescan of tests triggered (${reason}) - ${this.configFile.uri}}`);
this.recreateDiscoverer();
await this.scanFiles();
} catch (e) {
this.logChannel.error(e as Error, 'Failed to rescan tests');
}
};
this.disposables.add(this.configFile.onDidChange(() => rescan('mocharc changed')));
this.disposables.add(this.settings.onDidChange(() => rescan('settings changed')));
ctrl.refreshHandler = () => {
this.configFile.forget();
rescan('user');
};
this.scanFiles();
this.configFile.tryActivate();
}

recreateDiscoverer(newTsConfig: boolean = true) {
if (!this.ctrl) {
this.logChannel.trace('Skipping discoverer recreation, mocha is not active in this project.');
return;
}

if (!this.tsconfigStore) {
newTsConfig = true;
}
Expand Down Expand Up @@ -209,7 +228,7 @@ export class Controller {

let tree: IParsedNode[];
try {
tree = await this.discoverer.discover(uri.fsPath, contents);
tree = await this.discoverer!.discover(uri.fsPath, contents);
} catch (e) {
this.logChannel.error(
'Error while test extracting ',
Expand Down Expand Up @@ -242,7 +261,7 @@ export class Controller {
): vscode.TestItem => {
let item = parent.children.get(node.name);
if (!item) {
item = this.ctrl.createTestItem(node.name, node.name, start.uri);
item = this.ctrl!.createTestItem(node.name, node.name, start.uri);
counter.add(node.kind);
testMetadata.set(item, {
type: node.kind === NodeKind.Suite ? ItemType.Suite : ItemType.Test,
Expand Down Expand Up @@ -305,7 +324,7 @@ export class Controller {
for (const [id, test] of previous.items) {
if (!newTestsInFile.has(id)) {
const meta = testMetadata.get(test);
(test.parent?.children ?? this.ctrl.items).delete(id);
(test.parent?.children ?? this.ctrl!.items).delete(id);
if (meta?.type === ItemType.Test) {
counter.remove(NodeKind.Test);
} else if (meta?.type === ItemType.Suite) {
Expand Down Expand Up @@ -337,7 +356,7 @@ export class Controller {
let last: vscode.TestItemCollection | undefined;
for (const { children, item } of itemsIt) {
if (item && children.size === 1) {
deleteFrom ??= { items: last || this.ctrl.items, id: item.id };
deleteFrom ??= { items: last || this.ctrl!.items, id: item.id };
} else {
deleteFrom = undefined;
}
Expand All @@ -352,7 +371,7 @@ export class Controller {
if (deleteFrom) {
deleteFrom.items.delete(deleteFrom.id);
} else {
last!.delete(id);
last?.delete(id);
}
}

Expand Down Expand Up @@ -384,18 +403,22 @@ export class Controller {
for (const key of this.testsInFiles.keys()) {
this.deleteFileTests(key);
}
const item = (this.errorItem = this.ctrl.createTestItem('error', 'Extension Test Error'));
const item = (this.errorItem = this.ctrl!.createTestItem('error', 'Extension Test Error'));
item.error = new vscode.MarkdownString(
`[View details](command:${showConfigErrorCommand}?${encodeURIComponent(
JSON.stringify([this.configFile.uri.toString()]),
)})`,
);
item.error.isTrusted = true;
this.ctrl.items.add(item);
this.ctrl!.items.add(item);
}

/** Creates run profiles for each configuration in the extension tests */
private applyRunHandlers() {
if (!this.ctrl) {
return;
}

const oldRunHandlers = this.runProfiles;
this.runProfiles = new Map();
const originalName = 'Mocha Config';
Expand Down Expand Up @@ -446,6 +469,11 @@ export class Controller {
}

public async scanFiles() {
if (!this.ctrl) {
this.logChannel.trace('Skipping file scan, mocha is not active in this project.');
return;
}

if (this.errorItem) {
this.ctrl.items.delete(this.errorItem.id);
this.errorItem = undefined;
Expand Down Expand Up @@ -502,6 +530,9 @@ export class Controller {

/** Gets the test collection for a file of the given URI, descending from the root. */
private getContainingItemsForFile(uri: vscode.Uri, createOpts?: ICreateOpts) {
if (!this.ctrl) {
return [];
}
return getContainingItemsForFile(this.configFile.uri, this.ctrl, uri, createOpts);
}
}
Expand Down
32 changes: 23 additions & 9 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as timers from 'timers/promises';
import * as vscode from 'vscode';
import { ConfigValue } from './configValue';
import { ConsoleOuputChannel } from './consoleLogChannel';
import { getControllersForTestCommand } from './constants';
import { getControllersForTestCommand, recreateControllersForTestCommand } from './constants';
import { initESBuild } from './esbuild';
import { TestRunner } from './runner';
import { SourceMapStore } from './source-map-store';
Expand Down Expand Up @@ -103,9 +103,7 @@ export function activate(context: vscode.ExtensionContext) {
}
};

const initialSync = (async () => {
await initESBuild(context, logChannel);

async function syncWorkspaceFoldersWithRetry() {
// Workaround for vscode#179203 where findFiles doesn't work on startup.
// This extension is only activated on workspaceContains, so we have pretty
// high confidence that we should find something.
Expand All @@ -118,15 +116,31 @@ export function activate(context: vscode.ExtensionContext) {

await timers.setTimeout(1000);
}
}

const initialSync = (async () => {
await initESBuild(context, logChannel);
await syncWorkspaceFoldersWithRetry();
})();

context.subscriptions.push(
vscode.workspace.onDidChangeWorkspaceFolders(syncWorkspaceFolders),
vscode.commands.registerCommand(getControllersForTestCommand, () =>
initialSync.then(() =>
Array.from(watchers.values()).flatMap((w) => Array.from(w.controllers.values())),
),
),
vscode.commands.registerCommand(getControllersForTestCommand, async () => {
await initialSync;
return Array.from(watchers.values()).flatMap((w) => Array.from(w.controllers.values()));
}),
vscode.commands.registerCommand(recreateControllersForTestCommand, async () => {
logChannel.debug('Destroying all watchers and test controllers');
for (const [, watcher] of watchers) {
watcher.dispose();
}
watchers.clear();
resyncState = FolderSyncState.Idle;

logChannel.debug('Destroyed controllers, recreating');
await syncWorkspaceFoldersWithRetry();
return Array.from(watchers.values()).flatMap((w) => Array.from(w.controllers.values()));
}),
new vscode.Disposable(() => watchers.forEach((c) => c.dispose())),
logChannel,
);
Expand Down
4 changes: 2 additions & 2 deletions src/test/integration/config-file-change.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('config-file-change', () => {

// scan and test results
expect(c).to.not.be.undefined;
c!.scanFiles();
await c!.scanFiles();
expectTestTree(c!, [
['folder', [['nested.test.js', [['is nested']]]]],
['hello.test.js', [['math', [['addition'], ['subtraction']]]]],
Expand Down Expand Up @@ -100,7 +100,7 @@ describe('config-file-change', () => {

// scan and test results
expect(c).to.not.be.undefined;
c!.scanFiles();
await c!.scanFiles();
expectTestTree(c!, [['hello.test.js', [['math', [['addition'], ['subtraction']]]]]]);
});
});
2 changes: 1 addition & 1 deletion src/test/integration/overlapping-tests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ describe('overlapping tests', () => {
const profiles = c.profiles;
expect(profiles).to.have.lengthOf(2);

const testItems = include.map((i) => findTestItem(c.ctrl.items, i)!);
const testItems = include.map((i) => findTestItem(c.ctrl!.items, i)!);

const run = await captureTestRun(
c,
Expand Down
Loading
Loading