Skip to content

Commit 785cc82

Browse files
committed
feat: add testing framework and initial tests for ERB inline JS helper
- Updated package.json to include test scripts and dependencies for Mocha and VSCode testing. - Changed TypeScript configuration to use NodeNext module resolution. - Added runTest.ts to handle VSCode test execution and extension installation. - Created extension.test.ts with tests for activation, completion, hover, and definition features. - Implemented index.ts to run Mocha tests. - Added test fixtures including a sample Ruby workspace with Gemfile and example ERB file.
1 parent c4bd152 commit 785cc82

File tree

12 files changed

+505
-11
lines changed

12 files changed

+505
-11
lines changed

.github/workflows/ci.yml

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
1-
name: ci
2-
on: push
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
37
jobs:
4-
ci:
8+
e2e:
59
runs-on: ubuntu-latest
10+
timeout-minutes: 20
611
steps:
7-
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
8-
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
9-
- uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
12+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
13+
- uses: oven-sh/setup-bun@f4d14e03ff726c06358e5557344e1da148b56cf7 # v1.2.2
1014
with:
11-
bun-version-file: package.json
15+
bun-version: "1.3.5"
16+
- uses: ruby/setup-ruby@90be1154f987f4dc0fe0dd0feedac9e473aa4ba8 # v1.286.0
17+
with:
18+
ruby-version: "3.3"
19+
- run: gem install ruby-lsp
1220
- run: bun install
1321
- run: bun run build
22+
- run: xvfb-run -a bun run test:e2e

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ node_modules
22
*.vsix
33
.env
44
dist
5+
.vscode-test

bun.lock

Lines changed: 238 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,21 @@
5050
"main": "./dist/extension.js",
5151
"scripts": {
5252
"build": "bun x tsc -p tsconfig.json",
53-
"watch": "bun x tsc -w -p tsconfig.json"
53+
"watch": "bun x tsc -w -p tsconfig.json",
54+
"pretest": "bun x tsc -p tsconfig.json",
55+
"test": "bun run test:e2e",
56+
"test:e2e": "node ./dist/test/runTest.js"
5457
},
5558
"packageManager": "bun@1.3.5",
5659
"dependencies": {
5760
"typescript": "^5.9.3"
5861
},
5962
"devDependencies": {
63+
"@types/debug": "^4.1.12",
64+
"@types/mocha": "^10.0.10",
6065
"@types/node": "^25.0.3",
61-
"@types/vscode": "^1.107.0"
66+
"@types/vscode": "^1.107.0",
67+
"@vscode/test-electron": "^2.4.3",
68+
"mocha": "^10.8.2"
6269
}
6370
}

src/test/runTest.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { execFile } from 'child_process';
2+
import * as path from 'path';
3+
import { promisify } from 'util';
4+
5+
import {
6+
downloadAndUnzipVSCode,
7+
resolveCliPathFromVSCodeExecutablePath,
8+
runTests,
9+
} from '@vscode/test-electron';
10+
11+
const execFileAsync = promisify(execFile);
12+
const DEPENDENCY_EXTENSIONS = ['Shopify.ruby-lsp'];
13+
14+
async function installExtension(cliPath: string, extensionsDir: string, extensionId: string) {
15+
await execFileAsync(cliPath, [
16+
'--extensions-dir',
17+
extensionsDir,
18+
'--install-extension',
19+
extensionId,
20+
]);
21+
}
22+
23+
async function main() {
24+
try {
25+
const extensionDevelopmentPath = path.resolve(__dirname, '../..');
26+
const extensionTestsPath = path.resolve(__dirname, './suite/index');
27+
const workspacePath = path.resolve(
28+
extensionDevelopmentPath,
29+
'test-fixtures/ruby-workspace'
30+
);
31+
const extensionsDir = path.resolve(
32+
extensionDevelopmentPath,
33+
'.vscode-test/extensions'
34+
);
35+
const vscodeExecutablePath = await downloadAndUnzipVSCode('stable');
36+
const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath);
37+
38+
for (const extensionId of DEPENDENCY_EXTENSIONS) {
39+
await installExtension(cliPath, extensionsDir, extensionId);
40+
}
41+
42+
await runTests({
43+
extensionDevelopmentPath,
44+
extensionTestsPath,
45+
vscodeExecutablePath,
46+
launchArgs: [workspacePath, '--extensions-dir', extensionsDir],
47+
});
48+
} catch (error) {
49+
console.error('Failed to run extension tests.');
50+
console.error(error);
51+
process.exit(1);
52+
}
53+
}
54+
55+
void main();

src/test/suite/extension.test.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import * as assert from 'assert';
2+
import * as vscode from 'vscode';
3+
4+
const FIXTURE_RELATIVE_PATH = 'app/views/example.html.erb';
5+
6+
suite('ERB Inline JS Helper - Happy Path', () => {
7+
let document: vscode.TextDocument;
8+
9+
suiteSetup(async function () {
10+
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
11+
assert.ok(workspaceRoot, 'Workspace is not open');
12+
13+
document = await vscode.workspace.openTextDocument(vscode.Uri.file(`${workspaceRoot}/${FIXTURE_RELATIVE_PATH}`));
14+
15+
const extension = vscode.extensions.getExtension('kudoas.erb-inline-js-helper');
16+
assert.ok(extension, 'Extension is not found');
17+
await extension.activate();
18+
assert.ok(extension.isActive, 'Extension did not activate');
19+
});
20+
21+
test('extension activates', async function () {
22+
const extension = vscode.extensions.getExtension('kudoas.erb-inline-js-helper');
23+
24+
assert.ok(extension, 'Extension is not found');
25+
await extension.activate();
26+
assert.ok(extension.isActive, 'Extension did not activate');
27+
});
28+
29+
test('completion provides console.log', async function () {
30+
const position = positionAtSubstring(document, 'console.', 0, 'completion target', 'after');
31+
32+
const list = await vscode.commands.executeCommand<vscode.CompletionList>(
33+
'vscode.executeCompletionItemProvider',
34+
document.uri,
35+
position,
36+
'.'
37+
);
38+
39+
assert.ok(list, 'Completion list is undefined');
40+
const labels = list.items.map((item) => (typeof item.label === 'string' ? item.label : item.label.label));
41+
assert.ok(labels.includes('log'), 'Expected "log" in completion items');
42+
});
43+
44+
test('hover shows info for message', async function () {
45+
const position = positionAtSubstring(document, 'message', 0, 'hover target');
46+
const hovers = await vscode.commands.executeCommand<vscode.Hover[]>(
47+
'vscode.executeHoverProvider',
48+
document.uri,
49+
position
50+
);
51+
52+
assert.ok(hovers && hovers.length > 0, 'Hover is empty');
53+
const text = hovers
54+
.flatMap((hover) => hover.contents)
55+
.map((content) => (typeof content === 'string' ? content : content.value))
56+
.join(' ');
57+
58+
assert.ok(text.includes('message'), 'Hover does not mention "message"');
59+
});
60+
61+
test('definition points to message declaration', async function () {
62+
const usageLineIndex = indexOfSubstring(document, 'console.log(message);', 0, 'definition usage line');
63+
const usageNameIndex = indexOfSubstring(document, 'message', usageLineIndex, 'definition usage');
64+
const usagePosition = document.positionAt(usageNameIndex);
65+
const definitions = await vscode.commands.executeCommand<
66+
vscode.Location[] | vscode.Location | vscode.LocationLink[]
67+
>('vscode.executeDefinitionProvider', document.uri, usagePosition);
68+
69+
const locations = normalizeLocations(definitions);
70+
assert.ok(locations.length > 0, 'Definition result is empty');
71+
72+
const declarationLineIndex = indexOfSubstring(document, 'const message', 0, 'definition declaration');
73+
const declarationNameIndex = indexOfSubstring(
74+
document,
75+
'message',
76+
declarationLineIndex,
77+
'definition declaration name'
78+
);
79+
const declarationNamePosition = document.positionAt(declarationNameIndex);
80+
81+
const match = locations.find(
82+
(location) =>
83+
location.uri.toString() === document.uri.toString() && location.range.start.isEqual(declarationNamePosition)
84+
);
85+
86+
assert.ok(match, 'Definition did not resolve to the message declaration');
87+
});
88+
});
89+
90+
function positionAtSubstring(
91+
document: vscode.TextDocument,
92+
substring: string,
93+
fromIndex: number,
94+
label: string,
95+
position: 'start' | 'after' = 'start'
96+
): vscode.Position {
97+
const index = indexOfSubstring(document, substring, fromIndex, label);
98+
const offset = position === 'after' ? substring.length : 0;
99+
return document.positionAt(index + offset);
100+
}
101+
102+
function indexOfSubstring(document: vscode.TextDocument, substring: string, fromIndex: number, label: string): number {
103+
const text = document.getText();
104+
const index = text.indexOf(substring, fromIndex);
105+
assert.ok(index !== -1, `Missing substring for ${label}: ${substring}`);
106+
return index;
107+
}
108+
109+
function normalizeLocations(
110+
result: vscode.Location[] | vscode.Location | vscode.LocationLink[] | undefined
111+
): vscode.Location[] {
112+
if (!result) {
113+
return [];
114+
}
115+
116+
if (Array.isArray(result)) {
117+
return result.map((entry) => {
118+
if ('targetUri' in entry) {
119+
return new vscode.Location(entry.targetUri, entry.targetRange);
120+
}
121+
return entry;
122+
});
123+
}
124+
125+
return [result];
126+
}

src/test/suite/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
4+
import Mocha from 'mocha';
5+
6+
export function run(): Promise<void> {
7+
const mocha = new Mocha({
8+
color: true,
9+
ui: 'tdd',
10+
timeout: 10000
11+
});
12+
13+
const testsRoot = __dirname;
14+
const testFiles = fs
15+
.readdirSync(testsRoot)
16+
.filter((file) => file.endsWith('.test.js'))
17+
.map((file) => path.join(testsRoot, file));
18+
19+
for (const file of testFiles) {
20+
mocha.addFile(file);
21+
}
22+
23+
return new Promise((resolve, reject) => {
24+
mocha.run((failures) => {
25+
if (failures > 0) {
26+
reject(new Error(`${failures} tests failed.`));
27+
return;
28+
}
29+
30+
resolve();
31+
});
32+
});
33+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"rubyLsp.erbSupport": false
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
source "https://rubygems.org"
2+
3+
gem "ruby-lsp"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
ruby-lsp (0.18.1)
5+
6+
PLATFORMS
7+
ruby
8+
9+
DEPENDENCIES
10+
ruby-lsp
11+
12+
BUNDLED WITH
13+
2.5.0

0 commit comments

Comments
 (0)