Skip to content

Commit 9319648

Browse files
committed
Refactor unit tests to use shared vscode mock and clean up test setup:
- Extracted shared fakeVscode.ts with installFakeVscode() - Remove TS_NODE_COMPILER_OPTIONS from test-unit script in package.json since tsconfig.json already specifies module: commonjs - Simplify addUserSelectedPath Test 1 — remove unnecessary existsSyncStub and readFileStub wrappers since createLibertyProject is fully replaced with a fake and no real file I/O occurs
1 parent 2d59a3d commit 9319648

4 files changed

Lines changed: 98 additions & 167 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@
254254
"test": "npm run test-compile && extest setup-and-run -o .vscode/settings.json 'out/test/*.js' --code_settings src/test/resources/settings.json",
255255
"test-maven": "npm run test-compile && extest setup-and-run -o .vscode/settings.json 'out/test/M*.js' --code_settings src/test/resources/settings.json",
256256
"test-gradle": "npm run test-compile && extest setup-and-run -o .vscode/settings.json 'out/test/G*.js' --code_settings src/test/resources/settings.json",
257-
"test-unit": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' node --require ts-node/register node_modules/.bin/mocha --config .mocharc.unit.yml"
257+
"test-unit": "node node_modules/.bin/mocha --config .mocharc.unit.yml"
258258
},
259259
"devDependencies": {
260260
"@types/chai": "4.3.4",

src/test/unit/addProjectsToTheDashBoard.test.ts

Lines changed: 9 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -4,75 +4,28 @@
44
*/
55

66
// This test runs in plain Node, not inside a real VS Code window.
7-
// The extension's localization helper expects this env var to exist during import.
8-
process.env.VSCODE_NLS_CONFIG = JSON.stringify({ locale: "en" });
9-
10-
// Intercept module loading so we can replace VS Code and localization imports
11-
// with tiny fakes that are easy to reason about in a unit test.
12-
// eslint-disable-next-line @typescript-eslint/no-var-requires
13-
const Module = require("module");
14-
const originalLoad = Module._load;
15-
16-
// Fake localization helper.
17-
// Instead of reading translation files, return a predictable string that includes:
18-
// 1. the message key the command asked for
19-
// 2. the selected project path passed into localize()
20-
// Example result: "add.project.manually.message.0:/my/project"
21-
const localizeStub = (key: string, selection: string) => `${key}:${selection}`;
22-
23-
// Return a tiny fake module whenever production code imports "vscode" or the i18n helper.
24-
// devCommands.ts imports libertyProject.ts, and that file defines classes that extend
25-
// vscode.TreeItem and create vscode.EventEmitter instances. So this fake must include
26-
// enough of the VS Code API for those imports to load successfully.
27-
Module._load = function(request: string, ...args: any[]) {
28-
if (request === "vscode") {
29-
return {
30-
EventEmitter: class { event = () => {}; fire() {} },
31-
TreeItemCollapsibleState: { None: 0 },
32-
TreeItem: class {
33-
label: string;
34-
collapsibleState: number;
35-
tooltip?: string;
36-
constructor(label: string, collapsibleState?: number) {
37-
this.label = label;
38-
this.collapsibleState = collapsibleState ?? 0;
39-
}
40-
},
41-
Uri: { file: (p: string) => ({ fsPath: p }) },
42-
window: {
43-
showInformationMessage: showInformationMessageStub,
44-
setStatusBarMessage: () => ({ dispose() {} })
45-
}
46-
};
47-
}
48-
if (request.endsWith("/util/i18nUtil") || request === "../util/i18nUtil") {
49-
return {
50-
localize: localizeStub
51-
};
52-
}
53-
return originalLoad.call(this, request, ...args);
54-
};
55-
56-
import * as assert from "assert";
7+
import { installFakeVscode } from "./fakeVscode";
578
import * as sinon from "sinon";
589

59-
// Keep one shared reference to the fake VS Code message API.
60-
// The fake vscode module below returns this same stub, so each test can inspect
61-
// whether addProjectsToTheDashBoard() asked VS Code to show the expected message.
10+
// Keep one shared reference to the fake VS Code message API so each test can
11+
// inspect whether addProjectsToTheDashBoard() asked VS Code to show the right message.
6212
const showInformationMessageStub = sinon.stub();
6313

14+
// Install the shared vscode fake, must happen before any extension imports.
15+
// Pass our sinon stub so the fake window.showInformationMessage is observable.
16+
installFakeVscode({ showInformationMessage: showInformationMessageStub });
17+
18+
import * as assert from "assert";
6419
import { addProjectsToTheDashBoard } from "../../liberty/devCommands";
6520

6621
describe("addProjectsToTheDashBoard", () => {
6722

6823
afterEach(() => {
69-
// Reset call history on the shared message stub between tests.
7024
showInformationMessageStub.resetHistory();
71-
// Restore console stubs so each test starts clean.
7225
sinon.restore();
7326
});
7427

75-
// Run the same command-wrapper test for all 3 return codes from addUserSelectedPath():
28+
// Run the same wrapper test for all 3 return codes from addUserSelectedPath():
7629
// 0 = added successfully
7730
// 1 = project already exists
7831
// 2 = not a Maven or Gradle project
@@ -82,42 +35,23 @@ describe("addProjectsToTheDashBoard", () => {
8235
{ result: 2, expectedMessage: "add.project.manually.message.2:/my/project" }
8336
].forEach(({ result, expectedMessage }) => {
8437
it(`shows the expected message and refreshes the dashboard when addUserSelectedPath returns ${result}`, async () => {
85-
// Pretend the lower-level worker method already finished and returned this result.
86-
// This keeps the test focused on the command wrapper behavior only.
8738
const addUserSelectedPathStub = sinon.stub().resolves(result);
88-
89-
// addProjectsToTheDashBoard() passes projectProvider.getProjects() into addUserSelectedPath().
90-
// Return an empty map because the command wrapper does not care about its contents here.
9139
const getProjectsStub = sinon.stub().returns(new Map());
92-
93-
// The command should tell the dashboard tree to refresh after it handles the result.
9440
const fireChangeEventStub = sinon.stub();
95-
96-
// The command logs success with console.info() and non-success with console.error().
9741
const consoleInfoStub = sinon.stub(console, "info");
9842
const consoleErrorStub = sinon.stub(console, "error");
9943

100-
// Fake ProjectProvider with only the methods addProjectsToTheDashBoard() actually calls.
10144
const projectProvider = {
10245
addUserSelectedPath: addUserSelectedPathStub,
10346
getProjects: getProjectsStub,
10447
fireChangeEvent: fireChangeEventStub
10548
} as any;
10649

107-
// Call the real command wrapper under test.
10850
await addProjectsToTheDashBoard(projectProvider, "/my/project");
10951

110-
// ASSERT: the command should pass the selected path and live projects map
111-
// into addUserSelectedPath().
11252
assert.equal(addUserSelectedPathStub.calledOnceWithExactly("/my/project", getProjectsStub.firstCall.returnValue), true);
113-
114-
// ASSERT: the dashboard should be refreshed after the add attempt.
11553
assert.equal(fireChangeEventStub.calledOnce, true);
116-
117-
// ASSERT: the user should see the message built from the result code.
11854
assert.equal(showInformationMessageStub.calledOnceWithExactly(expectedMessage), true);
119-
120-
// ASSERT: success logs to console.info(); non-success logs to console.error().
12155
assert.equal(result === 0 ? consoleInfoStub.calledOnceWithExactly(expectedMessage) : consoleErrorStub.calledOnceWithExactly(expectedMessage), true);
12256
});
12357
});

src/test/unit/addUserSelectedPath.test.ts

Lines changed: 14 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,26 @@
44
*/
55

66
// This test runs in plain Node, not inside a real VS Code window.
7-
// Set the minimum localization config early so extension imports do not crash.
8-
process.env.VSCODE_NLS_CONFIG = JSON.stringify({ locale: "en" });
9-
10-
// When production code imports "vscode", return a tiny fake object instead.
11-
// We only provide the pieces ProjectProvider touches during this test.
12-
// eslint-disable-next-line @typescript-eslint/no-var-requires
13-
const Module = require("module");
14-
const originalLoad = Module._load;
15-
Module._load = function(request: string, ...args: any[]) {
16-
if (request === "vscode") {
17-
return {
18-
// ProjectProvider creates an EventEmitter in its constructor.
19-
EventEmitter: class { event = () => {}; fire() {} },
20-
TreeItemCollapsibleState: { None: 0 },
21-
TreeItem: class { constructor(label: string) { (this as any).label = label; } },
22-
Uri: { file: (p: string) => ({ fsPath: p }) },
23-
// refresh() temporarily shows a status bar message and later disposes it.
24-
window: {
25-
setStatusBarMessage: () => ({ dispose() {} })
26-
}
27-
};
28-
}
29-
return originalLoad.call(this, request, ...args);
30-
};
7+
import { installFakeVscode } from "./fakeVscode";
8+
import * as sinon from "sinon";
319

10+
// Install the shared vscode fake — must happen before any extension imports.
11+
installFakeVscode();
3212

3313
import * as assert from "assert";
34-
import * as sinon from "sinon";
35-
3614
import { ProjectProvider } from "../../liberty/libertyProject";
3715
import { DashboardData } from "../../liberty/dashboard";
3816

3917
// Fake ExtensionContext for ProjectProvider.
40-
// addUserSelectedPath() reads saved dashboard data from workspaceState and saves updates back to it.
18+
// addUserSelectedPath() reads saved dashboard data from workspaceState and saves updates back.
4119
const fakeContext: any = {
4220
workspaceState: {
43-
// Start with empty stored dashboard data.
4421
get: () => new DashboardData([], []),
45-
// Stubbed so each test can check whether a save happened.
4622
update: sinon.stub()
4723
},
4824
globalState: {
4925
get: () => undefined,
50-
update: sinon.stub(),
26+
update: sinon.stub()
5127
}
5228
};
5329

@@ -56,112 +32,59 @@ describe("addUserSelectedPath", () => {
5632
let provider: ProjectProvider;
5733

5834
before(() => {
59-
// Create the real ProjectProvider instance that owns addUserSelectedPath().
6035
provider = new ProjectProvider(fakeContext);
6136
});
6237

6338
afterEach(() => {
64-
// Reset Sinon-created stubs so one test's fake behavior does not leak into another.
6539
sinon.restore();
6640
});
6741

6842
// ─── Test 1 ───────────────────────────────────────────────────────────────
69-
// Scenario: the user manually picks a Maven project that is not already in the dashboard.
70-
// The add should succeed, update the live project map, and save the change.
43+
// Scenario: the user manually picks a Maven project not already in the dashboard.
7144
it("returns 0 and adds project to map when valid pom.xml exists", async () => {
45+
(provider as any).createLibertyProject = async () => ({
46+
getPath: () => "/my/project/pom.xml",
47+
getLabel: () => "my-app",
48+
getContextValue: () => "libertyMavenProject"
49+
});
7250

73-
// These two stubs model the successful Maven path inside createLibertyProject().
74-
// existsSyncStub() means "pretend pom.xml exists".
75-
const existsSyncStub = sinon.stub().returns(true);
76-
// readFileStub() means "pretend reading pom.xml returned this XML".
77-
const readFileStub = sinon.stub().resolves(`<project><artifactId>my-app</artifactId></project>`);
78-
79-
// Replace ProjectProvider's internal createLibertyProject() helper for this test only.
80-
// That keeps the test focused on addUserSelectedPath(): when project creation succeeds,
81-
// does it add the project, save it, and return the success code?
82-
(provider as any).createLibertyProject = async () => {
83-
if (existsSyncStub()) {
84-
await readFileStub();
85-
// Return the smallest fake project object addUserSelectedPath() needs.
86-
return {
87-
getPath: () => "/my/project/pom.xml",
88-
getLabel: () => "my-app",
89-
getContextValue: () => "libertyMavenProject"
90-
};
91-
}
92-
// If project creation fails, the real helper would return undefined.
93-
return undefined;
94-
};
95-
96-
// This map represents the live set of projects currently shown in the Liberty dashboard.
97-
// Start empty to model "project was not already in the dashboard".
9851
const existingProjects = new Map();
99-
100-
// Call the real method under test.
10152
const result = await provider.addUserSelectedPath("/my/project", existingProjects);
10253

103-
// ASSERT
104-
// 0 means the manual add succeeded.
10554
assert.equal(result, 0);
106-
// The project should now appear in the live dashboard map.
10755
assert.equal(existingProjects.has("/my/project/pom.xml"), true);
108-
// The dashboard update should also be persisted to workspace storage.
10956
assert.equal(fakeContext.workspaceState.update.called, true);
11057
});
11158

11259
// ─── Test 2 ───────────────────────────────────────────────────────────────
11360
// Scenario: the user picks a folder that is already in the Liberty dashboard.
114-
// addUserSelectedPath() should detect the duplicate and refuse to add it again.
11561
it("returns 1 and adds nothing when project already exists", async () => {
116-
117-
// Clear any save calls left over from earlier tests.
11862
fakeContext.workspaceState.update.resetHistory();
11963

120-
// provider.getProjects() is the provider's internal "already in the dashboard" map.
121-
// Put this path in the map first to simulate a project that Liberty Tools already knows about.
12264
const dashboardProjects = provider.getProjects();
12365
dashboardProjects.set("/my/project/pom.xml", {} as any);
12466

125-
// This separate map is the live map passed into addUserSelectedPath().
126-
// It starts empty because this test is checking that nothing new gets added.
12767
const existingProjects = new Map();
128-
129-
// Try to manually add the same project again.
13068
const result = await provider.addUserSelectedPath("/my/project", existingProjects);
13169

132-
// ASSERT
133-
// 1 means "project already exists".
13470
assert.equal(result, 1);
135-
136-
// Because the project was a duplicate, addUserSelectedPath() should not add anything new.
13771
assert.equal(existingProjects.size, 0);
138-
139-
// Because nothing changed, it should not save anything to workspace storage.
14072
assert.equal(fakeContext.workspaceState.update.called, false);
14173

142-
// Clean up the provider's internal dashboard map so other tests stay isolated.
14374
dashboardProjects.clear();
14475
});
14576

14677
// ─── Test 3 ───────────────────────────────────────────────────────────────
147-
// Scenario: the user picks a folder that is not a Maven or Gradle project.
148-
// The add should fail without changing dashboard state.
78+
// Scenario: the user picks a folder with no pom.xml or build.gradle.
14979
it("returns 2 and adds nothing when no build file exists", async () => {
150-
151-
// Clear any save calls recorded by earlier tests.
15280
fakeContext.workspaceState.update.resetHistory();
153-
// Force project creation to fail to model a folder with no pom.xml or build.gradle.
15481
(provider as any).createLibertyProject = async () => undefined;
155-
82+
15683
const existingProjects = new Map();
15784
const result = await provider.addUserSelectedPath("/my/project", existingProjects);
15885

159-
// ASSERT
160-
// 2 means the selected folder is not a Maven or Gradle project.
16186
assert.equal(result, 2);
162-
// Nothing should be added to the live dashboard map.
16387
assert.equal(existingProjects.size, 0);
164-
// Nothing should be saved because the add failed.
16588
assert.equal(fakeContext.workspaceState.update.called, false);
16689
});
16790

src/test/unit/fakeVscode.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* IBM Confidential
3+
* Copyright IBM Corp. 2026
4+
*/
5+
6+
/**
7+
* Shared VS Code API fake for unit tests that run in plain Node (no VS Code window).
8+
*
9+
* Import this file before any extension code. It sets the NLS config env var,
10+
* installs Module._load hooks for "vscode" and the i18n helper, and returns
11+
* a mutable fake object tests can inspect and mutate.
12+
*/
13+
14+
// The extension's localization helper reads this env var on import.
15+
// Set it before any extension module loads.
16+
process.env.VSCODE_NLS_CONFIG = JSON.stringify({ locale: "en" });
17+
18+
// eslint-disable-next-line @typescript-eslint/no-var-requires
19+
const Module = require("module");
20+
const originalLoad = Module._load;
21+
22+
export interface FakeVscode {
23+
EventEmitter: any;
24+
TreeItemCollapsibleState: { None: number };
25+
TreeItem: any;
26+
Uri: { file: (p: string) => { fsPath: string } };
27+
window: {
28+
showInformationMessage: (...args: any[]) => any;
29+
setStatusBarMessage: () => { dispose(): void };
30+
};
31+
workspace: {
32+
workspaceFolders: any;
33+
name: any;
34+
};
35+
}
36+
37+
export function installFakeVscode(windowOverrides: Partial<FakeVscode["window"]> = {}): FakeVscode {
38+
const fakeVscode: FakeVscode = {
39+
EventEmitter: class { event = () => {}; fire() {} },
40+
TreeItemCollapsibleState: { None: 0 },
41+
TreeItem: class {
42+
label: string;
43+
collapsibleState: number;
44+
constructor(label: string, collapsibleState = 0) {
45+
this.label = label;
46+
this.collapsibleState = collapsibleState;
47+
}
48+
},
49+
Uri: { file: (p: string) => ({ fsPath: p }) },
50+
window: {
51+
showInformationMessage: () => {},
52+
setStatusBarMessage: () => ({ dispose() {} }),
53+
...windowOverrides
54+
},
55+
workspace: {
56+
workspaceFolders: undefined,
57+
name: undefined
58+
}
59+
};
60+
61+
Module._load = function(request: string, ...args: any[]) {
62+
if (request === "vscode") {
63+
return fakeVscode;
64+
}
65+
// Stub the i18n helper so tests get a predictable "<key>:<arg>" string
66+
// instead of crashing when NLS translation files are not available in plain Node.
67+
if (request.endsWith("/util/i18nUtil") || request === "../util/i18nUtil") {
68+
return { localize: (key: string, ...args2: any[]) => [key, ...args2].join(":") };
69+
}
70+
return originalLoad.call(this, request, ...args);
71+
};
72+
73+
return fakeVscode;
74+
}

0 commit comments

Comments
 (0)