[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:

The final TreeView: We have all our tests from the opened project and their results displayed with the icons (passed, failed and unknown step). We can run all tests with the play button on top or single features/scenarios with the context menu

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 type
  • label just a text
  • program represents the test executable
  • cwd 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.

  1. Launch the test executable
  2. run cucumber ./features where the cwd needs to be set in the same directory where the feature 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!

The final result of the TreeView

 

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

Previous
Previous

[C++] Static, Dynamic Polymorphism, CRTP and C++20’s Concepts

Next
Next

[TypeScript] VS Code API: Let’s Create A Tree-View (Part 2)