[TypeScript] VS Code API: Let’s Create A Tree-View (Part 3)
You can find the project here on GitHub, I added a tag part-3
for this article.
In this last part about TreeViews in VS Code we finished the implementation of the cucumber TreeView example. And this is now the final result:
So let's go through latest implementation where I added:
- A debug configuration (
launch.json
) - A
cucumber
class to launch the test program and execute the tests - A
tree_view_data
class to have better control over the data
VS Code Debuggers
Let's add a debug configuration to package.json
where we can specify fields. In this context we have four properties:
type
our custom config typelabel
just a textprogram
represents the test executablecwd
the current working directory from where we execute the cucumber command
// ...
"debuggers": [
{
"type": "cwt-cucumber",
"label": "cwt cucumber support",
"configurationAttributes": {
"launch": {
"properties": {
"program": {
"type": "string"
},
"cwd": {
"type": "string"
}
}
}
}
}
//...
Now we can use this debug configuration in ./.vscode/launch.json
in our project, which could then like this:
{
"version": "0.2.0",
"configurations": [
{
"name":"my first example",
"type":"cwt-cucumber",
"program":"${workspaceFolder}/build/bin/cucumber_example.exe",
}
]
}
To get access to the properties we'll define an interface and then we can easily read the configuration with:
interface debug_config {
type: string;
name: string;
program: string|undefined;
cwd: string;
}
//...
const configs = vscode.workspace.getConfiguration("launch").get("configurations") as Array<debug_config>;
// now we can use
// configs[0].cwd
// configs[0].program
// configs[0].type
// configs[0].name
Note: Within this example I'll consider only the first configuration in launch.json
with the array index 0.
Executing Cucumber Tests
In a C++ context we have compiled our tests into an own test executable.
- Launch the test executable
- run
cucumber ./features
where thecwd
needs to be set in the same directory where thefeature
directory is
Then, there are the following methods in our tree_view
class:
// this is called with the green play button on top of the tree view
private run_all_tests() {
this.internal_run(undefined);
}
// this is called from the context menu:
// we pass to the internal_test the feature file
// and if this is just a single scenario we append the line to it
private run_tree_item(item: tree_item) {
var feature = item.file;
if (item.is_scenario) {
feature += ':' + item.line.row;
}
this.internal_run(feature);
}
// now we create our cucumber object, if the feature is undefined, we run all tests
private internal_run(feature: string|undefined) {
var cucumber_runner = new cucumber(feature);
// once the tests are done, we set the test results and update the tree
// we'll come to that later
cucumber_runner.run_tests().then(() => {
cucumber_runner.set_test_results(this.data);
this.reload_tree_data();
});
}
To execute programs or terminal commands we use the spawn
function. The return type of run_tests
is a Promise
because we want to wait until the tests are done, when we update the icons on the TreeView.
// run_tests is an async function to use await and .then(..)
public async run_tests() {
// first we wait until our executable is up and running
await this.launch_program();
// second we run the cucumber terminal command
return this.execute_cucumber();
}
Then let's consider launch_program()
:
private launch_program() {
// let's create a variable of this, otherwise we don't have access
// to this inside the Promise constructor
var self = this;
return new Promise(function (resolve, reject) {
// create a runner which takes the test programm from launch.json
var runner = spawn(self.program, {detached: false});
// and when the program is running we call resolve to fulfill our promise
runner.on('spawn', () => {
console.log(self.program + ' started!');
resolve(true);
});
// if there's an error, we reject the promise (e.g. wrong program, no program, etc.)
runner.on('error', (code) =>{
console.log('error: ', code);
reject(code);
});
});
}
The execute_cucumber()
method is almost the same where we additionally have arguments and set the current working directory.
When we create / return a Promise
we need to call resolve
or reject
at some point. After the call we continue in .then(..)
, .catch(..)
, finally(..)
or after the await
keyword.
Evaluating The Test Results
For the test evaluation we run the cucumber command in execute_cucumber()
with the --format json
option. And just like we read the launch.json
we create an interface to read the results from stdout
:
// the interface with the properties we need
interface cucumber_results {
id: string;
uri: string;
elements: [{
line: number;
name: string;
steps: [{
result: {
status: string;
}
}]
}]
}
// ...
public set_test_results(tree_data: tree_view_data) {
var result = JSON.parse(this.test_result) as cucumber_results[];
// lets loop over all the results and set them accordingly
// if they're not failed / unknown they are passed by default
result.forEach((feature) => {
feature.elements.forEach((scenario) => {
var result = test_result.passed;
scenario.steps.forEach((step) => {
switch (step.result.status) {
case 'failed':
result = test_result.failed;
break;
case 'undefined':
result = test_result.undefined
break;
}
});
tree_data.get_scenario_by_uri_and_row(feature.uri, scenario.line)?.set_test_result(result);
});
});
}
Adding Icons To Tree Items
In general there are four different states:
enum test_result {
none, // test didn't run -> no icon
passed, // test passed -> green check
undefined, // test step not implemented -> yellow splash
failed // test failed -> red cross
}
And since we inherit from vscode.TreeItem
we have access to this.iconPath
which we just need to set:
public set_test_result(result: test_result) {
// in the implementation i take the correct png with respect to
// the test_result from the argument
this.iconPath = {
light: path.join(__filename, '..', '..', 'src', 'assets', 'icon.png'),
dark: path.join(__filename, '..', '..', 'src', 'assets', 'icon.png')
};
}
And thats basically it!
Conclusion
Feel free to checkout the source code of this Visual Studio Code Extension, you can find the GitHub repository here.
To test this plugin with cucumber I used my cucumber example, which you can find, download and build here on GitHub.
I really liked to work with TypeScript and the VS Code API. In my opinion, this demonstrates how fast and easy you can get a frontend done. Of course there are still a lot of edge cases which needs to be considered. In general UI/UX needs to be improved (more messages, user feedback, etc.) if I would run this as a real product.
As far as i noticed, the official cucumber plugin does not have a TreeView implemented by now. To do so, it would way more work to support all languages where I can use cucumber. For instance: If you use python, there is another command line tool needed (behave
or lettuce
), where also the json report looks different. Also in Java this works a bit different. All this is not impossible, it's just effort to do.
But I think this is a good example to learn how to create a TreeView in VS Code. And this ultimately was the intention of the three articles.
I hope this was helpfull and that's it for now.
Best Thomas