Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sidebar support #357

Closed
wants to merge 21 commits into from
Closed
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Bug-fixes within the same version aren't needed

## Master

* Added preliminary support for test sidebar - gstamac
* Your message here - name

-->
Expand Down
3 changes: 3 additions & 0 deletions __mocks__/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const commands = {
executeCommand: jest.fn(),
}

const TreeItem = jest.fn()

export {
languages,
StatusBarAlignment,
Expand All @@ -51,4 +53,5 @@ export {
DiagnosticSeverity,
debug,
commands,
TreeItem,
}
20 changes: 19 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@
"path": "./syntaxes/jest-snapshot.tmLanguage"
}
],
"views": {
"test": [
{
"id": "jest",
"name": "Jest"
}
]
},
"configuration": {
"type": "object",
"title": "Jest configuration",
Expand Down Expand Up @@ -118,6 +126,16 @@
"type": "boolean",
"default": false
},
"jest.sidebar.showFiles": {
"description": "Show filenames in sidebar tree view",
"type": "boolean",
"default": true
},
"jest.sidebar.autoExpand": {
"description": "Automatically expand tests in sidebar tree view",
"type": "boolean",
"default": true
},
"jest.debugMode": {
"description": "Enable debug mode to diagnose plugin issues. (see developer console)",
"type": "boolean",
Expand Down Expand Up @@ -204,7 +222,7 @@
},
"scripts": {
"precommit": "lint-staged",
"ci": "yarn lint && yarn test --coverage",
"ci": "yarn lint && yarn test --coverage --maxWorkers=4",
"clean-out": "rimraf ./out",
"vscode:prepublish": "yarn clean-out && tsc -p ./tsconfig.publish.json",
"compile": "tsc -watch -p ./tsconfig.publish.json",
Expand Down
16 changes: 16 additions & 0 deletions src/JestExt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { hasDocument, isOpenInMultipleEditors } from './editor'
import { CoverageOverlay } from './Coverage/CoverageOverlay'
import { JestProcess, JestProcessManager } from './JestProcessManagement'
import { isWatchNotSupported, WatchMode } from './Jest'
import { JestTreeProvider } from './SideBar/JestTreeProvider'
import * as messaging from './messaging'

export class JestExt {
Expand All @@ -34,6 +35,8 @@ export class JestExt {
public debugCodeLensProvider: DebugCodeLensProvider
debugConfigurationProvider: DebugConfigurationProvider

sidebarProvider: JestTreeProvider

// So you can read what's going on
private channel: vscode.OutputChannel

Expand Down Expand Up @@ -83,6 +86,8 @@ export class JestExt {
)
this.debugConfigurationProvider = new DebugConfigurationProvider()

this.sidebarProvider = new JestTreeProvider(this.testResultProvider, context, pluginSettings.sidebar)

this.jestProcessManager = new JestProcessManager({
projectWorkspace: workspace,
runAllTestsFirstInWatchMode: this.pluginSettings.runAllTestsFirst,
Expand Down Expand Up @@ -255,6 +260,8 @@ export class JestExt {
? updatedSettings.debugCodeLens.showWhenTestStateIn
: []

this.sidebarProvider.updateSettings(updatedSettings.sidebar)

this.stopProcess()

setTimeout(() => {
Expand Down Expand Up @@ -354,6 +361,7 @@ export class JestExt {

private testsHaveStartedRunning() {
this.channel.clear()
this.sidebarProvider.clear()
status.running('initial full test run')
}

Expand All @@ -364,6 +372,8 @@ export class JestExt {
const statusList = this.testResultProvider.updateTestResults(normalizedData)
updateDiagnostics(statusList, this.failDiagnostics)

this.sidebarProvider.refresh(data)
Copy link
Collaborator

@connectdotz connectdotz Aug 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if we should process all the tests up front like this, giving we will only need to display the tree when the file is actually displayed in the editor... please see if you can move the logic to triggerUpdateActiveEditor instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tree is displayed every time the tests are run not only when the file is selected. You probably mean the outline tree but what we are showing here is the test sidebar tree.

Copy link
Collaborator

@connectdotz connectdotz Aug 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, I was thinking about the outline view, my bad.

trying it again on vscode-jest itself, added a few fake tests and encountered the following issues:

  • looks like under the root "Tests" are the describe blocks instead of the file. So if a test file doesn't have a root describe block or it has multiple parallel describe blocks, they will not show any hierarchical relationship. Is that intended? Is there a particular reason we don't want to use the test file for grouping?
    • (update) ok, I saw you have a jest.showFilesInSidebar setting, however
      • when I update the setting, the sidebar display didn't change until I restart/reload vscode.
      • it displays the full path instead of the filename, which is by far the most important info but now is almost always truncated... can we find a way to display the filename first, maybe use tooltip or other visual hints for path if necessary?
      • on the other hand, I wonder if there is really a common use case without grouping the tests by file... maybe at least to make the default of jest.showFilesInSidebar to true?
  • the root level it blocks are not shown, and if reload/restart vscode, it crashed the plugin, put it in the never-ending spinning "Starting watch mode" and the Test sidebar is empty... (same error as mentioned in earlier comment.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added support for root tests, removed the folder name from filename and changed the default setting to show files by default. My practice has always been to have all tests inside describe and to have only one describe per file. That's why I chose the implementation I did.
I see you have to reload the window when the settings change but I think that's the case for all vscode-jest settings because getExtensionSettings is called on extension.activate event.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you have to reload the window when the settings change but I think that's the case for all vscode-jest settings because getExtensionSettings is called on extension.activate event.

Please take a look at triggerUpdateSettings


const failedFileCount = failedSuiteCount(this.failDiagnostics)
if (failedFileCount <= 0 && normalizedData.success) {
status.success()
Expand Down Expand Up @@ -423,6 +433,12 @@ export class JestExt {
}
}

public showTest = async (fileName: string, line: number) => {
const textRange = new vscode.Range(line, 0, line, 0)
const doc = await vscode.workspace.openTextDocument(fileName)
await vscode.window.showTextDocument(doc, { selection: textRange })
}

onDidCloseTextDocument(document: vscode.TextDocument) {
this.removeCachedTestResults(document)
this.removeCachedDecorationTypes(document)
Expand Down
4 changes: 4 additions & 0 deletions src/Settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export interface IPluginSettings {
showCoverageOnLoad: boolean
coverageFormatter: string
debugMode?: boolean
sidebar: {
showFiles: boolean
autoExpand: boolean
}
}

export function isDefaultPathToJest(str) {
Expand Down
162 changes: 162 additions & 0 deletions src/SideBar/JestTreeNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import * as vscode from 'vscode'
import { TestResultFile, TestResultSuite, TestResultTest } from './TestResultTree'
import { extensionName } from '../appGlobals'

export type NodeStatus = 'unknown' | 'passed' | 'failed' | 'skipped'

export class JestTreeNode extends vscode.TreeItem {
constructor(
label: string,
public readonly children: JestTreeNode[],
context: SidebarContext,
public contextValue: string = '',
public readonly status: NodeStatus = 'unknown'
) {
super(label, children.length > 0 ? context.getTreeItemCollapsibleState() : vscode.TreeItemCollapsibleState.None)

if (this.status === 'unknown') {
this.status = this.calculateStatus()
}

this.iconPath = context.getIconPath(this.status)
}

get tooltip(): string {
return this.terseTooltip
}

get terseTooltip(): string {
if (this.children.length > 0) {
return this.children.map(c => `${this.label} > ` + c.terseTooltip.replace(/\n/g, `\n${this.label} > `)).join('\n')
}
const prettyStatus = this.status.charAt(0).toUpperCase() + this.status.toLowerCase().slice(1)
return `${this.label} ● ${prettyStatus}`
}

calculateStatus(): NodeStatus {
if (this.children.length > 0) {
if (this.children.find(c => c.status === 'failed')) {
return 'failed'
}
if (this.children.find(c => c.status === 'skipped')) {
return 'skipped'
}
if (!this.children.find(c => c.status !== 'passed')) {
return 'passed'
}
}

return 'unknown'
}
}

export class JestTreeNodeForTest extends JestTreeNode {
constructor(private test: TestResultTest, context: SidebarContext) {
super(test.name, [], context, 'test', convertTestStatus(test.status))
}

get tooltip(): string {
if (this.test.failureMessages.length > 0) {
return `${this.terseTooltip}\n\n${this.test.failureMessages.join('\n')}`
}
return this.terseTooltip
}

get command(): vscode.Command {
return {
title: 'Show test',
command: `${extensionName}.show-test`,
arguments: [this.test.filename, this.test.line],
}
// codeLens.command = {
// arguments: [codeLens.fileName, escapeRegExp(codeLens.testName)],
// command: `${extensionName}.run-test`,
// title: 'Debug',
// }
}
}

export interface ISidebarSettings {
showFiles: boolean
autoExpand: boolean
}

export class SidebarContext {
showFiles: boolean
autoExpand: boolean

constructor(private extensionContext: vscode.ExtensionContext, settings: ISidebarSettings) {
this.updateSettings(settings)
}

updateSettings(settings: ISidebarSettings): void {
this.autoExpand = settings.autoExpand
this.showFiles = settings.showFiles
}

getTreeItemCollapsibleState() {
return this.autoExpand ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.Collapsed
}

getIconPath(iconColor: string) {
return {
light: this.extensionContext.asAbsolutePath('./src/SideBar/light-' + iconColor + '.svg'),
dark: this.extensionContext.asAbsolutePath('./src/SideBar/dark-' + iconColor + '.svg'),
}
}
}

function convertTestStatus(testStatus: 'failed' | 'passed' | 'pending'): NodeStatus {
return testStatus === 'pending' ? 'skipped' : testStatus
}

export function generateTree(files: undefined | TestResultFile[], context: SidebarContext): JestTreeNode {
const rootNode = new JestTreeNode(
'Tests',
files === undefined ? [] : getNodesFromFiles(files, context),
context,
'root'
)
rootNode.collapsibleState = vscode.TreeItemCollapsibleState.Expanded
return rootNode
}

function getNodesFromFiles(files: TestResultFile[], context: SidebarContext): JestTreeNode[] {
return [].concat(...files.map(f => getNodesFromFile(f, context)))
}

function getNodesFromFile(file: TestResultFile, context: SidebarContext): JestTreeNode[] {
if (context.showFiles) {
return [new JestTreeNode(cleanFilename(file.name), getNodesFromSuite(file.suite, context), context, 'file')]
} else {
if (file.suite.tests.length === 0) {
return getNodesFromSuite(file.suite, context)
} else {
return file.suite.suites
.map(s => new JestTreeNode(s.name, getNodesFromSuite(s, context), context, 'suite'))
.concat([
new JestTreeNode(
cleanFilename(file.name),
file.suite.tests.map(t => new JestTreeNodeForTest(t, context)),
context,
'file'
),
])
}
}
}

function cleanFilename(filename: string): string {
const file = vscode.Uri.file(filename)
const folder = vscode.workspace.getWorkspaceFolder(file)
if (folder && folder.uri && file.path.toLowerCase().startsWith(folder.uri.path.toLowerCase())) {
return file.path.substring(folder.uri.path.length + 1)
}
return filename
}

function getNodesFromSuite(suite: TestResultSuite, context: SidebarContext): JestTreeNode[] {
return suite.suites
.map(s => new JestTreeNode(s.name, getNodesFromSuite(s, context), context, 'suite'))
.concat(suite.tests.map(t => new JestTreeNodeForTest(t, context)))
}
74 changes: 74 additions & 0 deletions src/SideBar/JestTreeProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as vscode from 'vscode'
import { JestTotalResults, JestFileResults } from 'jest-editor-support'
import { JestTreeNode, SidebarContext, ISidebarSettings, generateTree } from './JestTreeNode'
import { TestResultFile } from './TestResultTree'
import { TestResultProvider } from '../TestResults'

export class JestTreeProvider implements vscode.TreeDataProvider<JestTreeNode> {
private _onDidChangeTreeData: vscode.EventEmitter<JestTreeNode | undefined> = new vscode.EventEmitter<
JestTreeNode | undefined
>()
readonly onDidChangeTreeData: vscode.Event<JestTreeNode | undefined> = this._onDidChangeTreeData.event

private context: SidebarContext
private rootNode: JestTreeNode
private allResults: JestFileResults[]

constructor(
private testResultProvider: TestResultProvider,
extensionContext: vscode.ExtensionContext,
settings: ISidebarSettings
) {
this.context = new SidebarContext(extensionContext, settings)
this.clear()
}

updateSettings(settings: ISidebarSettings): void {
this.context.updateSettings(settings)
}

clear(): void {
this.allResults = []
this.rootNode = generateTree(undefined, this.context)
this._onDidChangeTreeData.fire()
}

refresh(data: JestTotalResults): void {
this.loadTestResults(data)
this._onDidChangeTreeData.fire()
}

getTreeItem(element: JestTreeNode): vscode.TreeItem {
return element
}

getChildren(element?: JestTreeNode): JestTreeNode[] {
if (!element) {
return this.getRootElements()
} else {
return this.getElementChildren(element)
}
}

private loadTestResults(data: JestTotalResults) {
this.allResults = this.allResults
.filter(r => !data.testResults.find(r1 => r1.name === r.name))
.concat(data.testResults)
.sort((a, b) => a.name.localeCompare(b.name))
const testFiles = this.allResults.map(r => this.loadTestResultsForFile(r))
this.rootNode = generateTree(testFiles, this.context)
}

private loadTestResultsForFile(data: JestFileResults): TestResultFile {
const parsedResults = this.testResultProvider.getResults(data.name)
return new TestResultFile(data, parsedResults)
}

private getRootElements(): JestTreeNode[] {
return [this.rootNode]
}

private getElementChildren(node: JestTreeNode): JestTreeNode[] {
return node.children
}
}
Loading