import * as vscode from 'vscode' ; import { JestTotalResults } from 'jest-editor-support' ; import { TestStatus } from '../decorations/test-status' ; import { statusBar , StatusBar , Mode , StatusBarUpdate , SBTestStats } from '../StatusBar' ; import { TestReconciliationState , TestResultProvider , TestResult , resultsWithLowerCaseWindowsDriveLetters , SortedTestResults , TestResultStatusInfo , TestReconciliationStateType , } from '../TestResults' ; import { testIdString , IdStringType , escapeRegExp , emptyTestStats } from '../helpers' ; import { CoverageMapProvider , CoverageCodeLensProvider } from '../Coverage' ; import { updateDiagnostics , updateCurrentDiagnostics , resetDiagnostics } from '../diagnostics' ; import { DebugCodeLensProvider , DebugTestIdentifier } from '../DebugCodeLens' ; import { DebugConfigurationProvider } from '../DebugConfigurationProvider' ; import { DecorationOptions , TestStats } from '../types' ; import { CoverageOverlay } from '../Coverage/CoverageOverlay' ; import { resultsWithoutAnsiEscapeSequence } from '../TestResults/TestResult' ; import { CoverageMapData } from 'istanbul-lib-coverage' ; import { Logging } from '../logging' ; import { createProcessSession , ProcessSession } from './process-session' ; import { JestExtContext , JestSessionEvents , JestExtSessionContext , JestRunEvent } from './types' ; import * as messaging from '../messaging' ; import { SupportedLanguageIds } from '../appGlobals' ; import { createJestExtContext , getExtensionResourceSettings , prefixWorkspace } from './helper' ; import { PluginResourceSettings } from '../Settings' ; import { startWizard , WizardTaskId } from '../setup-wizard' ; import { JestExtExplorerContext } from '../test-provider/types' ; import { JestTestProvider } from '../test-provider' ; import { JestProcessInfo } from '../JestProcessManagement' ; interface RunTestPickItem extends vscode . QuickPickItem { id : DebugTestIdentifier ; } /** extract lines starts and end with [] */ export class JestExt { coverageMapProvider : CoverageMapProvider ; coverageOverlay : CoverageOverlay ; testResultProvider : TestResultProvider ; debugCodeLensProvider : DebugCodeLensProvider ; debugConfigurationProvider : DebugConfigurationProvider ; coverageCodeLensProvider : CoverageCodeLensProvider ; channel : vscode . OutputChannel ; private decorations : TestStatus ; private failDiagnostics : vscode . DiagnosticCollection ; private processSession : ProcessSession ; private vscodeContext : vscode . ExtensionContext ; private status : ReturnType < StatusBar [ 'bind' ] > ; private logging : Logging ; private extContext : JestExtContext ; private dirtyFiles : Set < string > = new Set ( ) ; private testProvider ? : JestTestProvider ; public events : JestSessionEvents ; constructor ( vscodeContext , workspaceFolder , debugCodeLensProvider , debugConfigurationProvider , coverageCodeLensProvider ) { const pluginSettings = getExtensionResourceSettings ( workspaceFolder . uri ) ; this . extContext = createJestExtContext ( workspaceFolder , pluginSettings ) ; this . logging = this . extContext . loggingFactory . create ( 'JestExt' ) ; this . channel = vscode . window . createOutputChannel ( `Jest (${ workspaceFolder . name })` ) ; this . failDiagnostics = vscode . languages . createDiagnosticCollection ( `Jest (${ workspaceFolder . name })` ) ; this . debugCodeLensProvider = debugCodeLensProvider ; this . coverageCodeLensProvider = coverageCodeLensProvider ; this . coverageMapProvider = new CoverageMapProvider ( ) ; this . vscodeContext = vscodeContext ; this . coverageOverlay = new CoverageOverlay ( vscodeContext , this . coverageMapProvider , pluginSettings . showCoverageOnLoad , pluginSettings . coverageFormatter , pluginSettings . coverageColors ) ; this . events = { onRunEvent : new vscode . EventEmitter < JestRunEvent > ( ) , onTestSessionStarted : new vscode . EventEmitter < JestExtSessionContext > ( ) , onTestSessionStopped : new vscode . EventEmitter < void > ( ) , } ; this . setupRunEvents ( this . events ) ; this . testResultProvider = new TestResultProvider ( this . events , pluginSettings . debugMode ?? false ) ; this . debugConfigurationProvider = debugConfigurationProvider ; this . status = statusBar . bind ( workspaceFolder . name ) ; this . decorations = new TestStatus ( vscodeContext ) ; resetDiagnostics ( this . failDiagnostics ) ; this . processSession = this . createProcessSession ( ) ; this . setupStatusBar ( ) ; } private getExtExplorerContext ( ) { return { ... this . extContext , sessionEvents : this . events , session : this . processSession , testResolveProvider : this . testResultProvider , debugTests : this . debugTests , } ; } private setupWizardAction ( taskId ) { return { title : 'Run Setup Wizard' , action : ( ) : unknown => startWizard ( this . debugConfigurationProvider , { workspace : this . extContext . workspace , taskId , verbose : this . extContext . settings . debugMode , } ) , } ; } private setupRunEvents ( events ) { events . onRunEvent . event ( ( event ) => { switch ( event . type ) { case 'scheduled' : this . channel . appendLine ( `${ event . process . id } is scheduled` ) ; break ; case 'data' : if ( event . newLine ) { this . channel . appendLine ( event . text ) ; } else { this . channel . append ( event . text ) ; } if ( event . isError ) { this . channel . show ( ) ; } break ; case 'start' : this . updateStatusBar ( { state : 'running' } ) ; this . channel . clear ( ) ; break ; case 'end' : this . updateStatusBar ( { state : 'done' } ) ; break ; case 'exit' : if ( event . error ) { this . updateStatusBar ( { state : 'stopped' } ) ; const msg = `${ event . error }\n see troubleshooting: ${ messaging . TROUBLESHOOTING_URL }` ; this . channel . appendLine ( msg ) ; this . channel . show ( ) ; messaging . systemErrorMessage ( event . error , messaging . showTroubleshootingAction , this . setupWizardAction ( 'cmdLine' ) ) ; } else { this . updateStatusBar ( { state : 'done' } ) ; } break ; } } ) ; } private createProcessSession ( ) { return createProcessSession ( { ... this . extContext , updateWithData : this . updateWithData . bind ( this ) , onRunEvent : this . events . onRunEvent , } ) ; } private toSBStats ( stats ) { return { ... stats , isDirty : this . dirtyFiles . size > 0 } ; } private setTestFiles ( list ) { this . testResultProvider . updateTestFileList ( list ) ; this . updateStatusBar ( { stats : this . toSBStats ( this . testResultProvider . getTestSuiteStats ( ) ) } ) ; } /**
     * starts a new session, notify all session-aware components and gather the metadata.
     */ public async startSession ( newSession = false ) { try { this . dirtyFiles . clear ( ) ; this . resetStatusBar ( ) ; if ( newSession ) { await this . processSession . stop ( ) ; this . processSession = this . createProcessSession ( ) ; this . channel . appendLine ( 'Starting a new Jest Process Session' ) ; } else { this . channel . appendLine ( 'Starting Jest Session' ) ; } this . testProvider ?. dispose ( ) ; if ( this . extContext . settings . testExplorer . enabled ) { this . testProvider = new JestTestProvider ( this . getExtExplorerContext ( ) ) ; } await this . processSession . start ( ) ; this . events . onTestSessionStarted . fire ( { ... this . extContext , session : this . processSession } ) ; this . updateTestFileList ( ) ; this . channel . appendLine ( 'Jest Session Started' ) ; } catch ( e ) { const msg = prefixWorkspace ( this . extContext , 'Failed to start jest session' ) ; this . logging ( 'error' , `${ msg }:` , e ) ; this . channel . appendLine ( 'Failed to start jest session' ) ; messaging . systemErrorMessage ( `${ msg }...` , messaging . showTroubleshootingAction , this . setupWizardAction ( 'cmdLine' ) ) ; } } public async stopSession ( ) { try { this . channel . appendLine ( 'Stopping Jest Session' ) ; await this . processSession . stop ( ) ; this . testProvider ?. dispose ( ) ; this . testProvider = undefined ; this . events . onTestSessionStopped . fire ( ) ; this . channel . appendLine ( 'Jest Session Stopped' ) ; this . updateStatusBar ( { state : 'stopped' } ) ; } catch ( e ) { const msg = prefixWorkspace ( this . extContext , 'Failed to stop jest session' ) ; this . logging ( 'error' , `${ msg }:` , e ) ; this . channel . appendLine ( 'Failed to stop jest session' ) ; messaging . systemErrorMessage ( '${msg}...' , messaging . showTroubleshootingAction ) ; } } /** update custom editor context used by vscode when clause, such as `jest:run.interactive` in package.json */ private updateEditorContext ( ) { vscode . commands . executeCommand ( 'setContext' , 'jest:run.interactive' , true ) ; } private updateTestFileEditor ( editor ) { if ( ! this . isTestFileEditor ( editor ) ) { return ; } const filePath = editor . document . fileName ; let sortedResults ; try { sortedResults = this . testResultProvider . getSortedResults ( filePath ) ; } catch ( e ) { this . channel . appendLine ( `${ filePath }: failed to parse test results: ${ e }` ) ; sortedResults = { fail : [ ] , skip : [ ] , success : [ ] , unknown : [ ] , } ; } if ( ! sortedResults ) { return ; } this . updateDecorators ( sortedResults , editor ) ; updateCurrentDiagnostics ( sortedResults . fail , this . failDiagnostics , editor ) ; } public triggerUpdateActiveEditor ( editor ) { this . updateEditorContext ( ) ; this . coverageOverlay . updateVisibleEditors ( ) ; this . updateTestFileEditor ( editor ) ; } public triggerUpdateSettings ( newSettings ? ) { const updatedSettings = newSettings ?? getExtensionResourceSettings ( this . extContext . workspace . uri ) ; this . testResultProvider . verbose = updatedSettings . debugMode ?? false ; const showCoverage = this . coverageOverlay . enabled ?? updatedSettings . showCoverageOnLoad ; updatedSettings . showCoverageOnLoad = showCoverage ; this . coverageOverlay . dispose ( ) ; this . coverageOverlay = new CoverageOverlay ( this . vscodeContext , this . coverageMapProvider , updatedSettings . showCoverageOnLoad , updatedSettings . coverageFormatter , updatedSettings . coverageColors ) ; this . extContext = createJestExtContext ( this . extContext . workspace , updatedSettings ) ; return this . startSession ( true ) ; } updateDecorators ( testResults , editor ) { if ( this . extContext . settings . testExplorer . enabled === false || this . extContext . settings . testExplorer . showClassicStatus ) { const styleMap = [ { data : testResults . success , decorationType : this . decorations . passing , state : TestReconciliationState . KnownSuccess , } , { data : testResults . fail , decorationType : this . decorations . failing , state : TestReconciliationState . KnownFail , } , { data : testResults . skip , decorationType : this . decorations . skip , state : TestReconciliationState . KnownSkip , } , { data : testResults . unknown , decorationType : this . decorations . unknown , state : TestReconciliationState . Unknown , } , ] ; styleMap . forEach ( ( style ) => { const decorators = this . generateDotsForItBlocks ( style . data , style . state ) ; editor . setDecorations ( style . decorationType , decorators ) ; } ) ; } this . debugCodeLensProvider . didChange ( ) ; } private isSupportedDocument ( document ) { if ( ! document ) { return false ; } return SupportedLanguageIds . includes ( document . languageId ) ; } private isTestFileEditor ( editor ) { if ( ! this . isSupportedDocument ( editor . document ) ) { return false ; } if ( this . testResultProvider . isTestFile ( editor . document . fileName ) === 'no' ) { return false ; } return true ; } public activate ( ) { if ( vscode . window . activeTextEditor ?. document . uri && vscode . workspace . getWorkspaceFolder ( vscode . window . activeTextEditor . document . uri ) === this . extContext . workspace ) { this . onDidChangeActiveTextEditor ( vscode . window . activeTextEditor ) ; } } public deactivate ( ) { this . stopSession ( ) ; this . channel . dispose ( ) ; this . testResultProvider . dispose ( ) ; this . testProvider ?. dispose ( ) ; this . events . onRunEvent . dispose ( ) ; this . events . onTestSessionStarted . dispose ( ) ; this . events . onTestSessionStopped . dispose ( ) ; } public debugTests = async ( document , ... ids ) : Promise < void > => { const idString = ( type , id ) : string => typeof id === 'string' ? id : testIdString ( type , id ) ; const selectTest = async ( testIdentifiers ) : Promise < DebugTestIdentifier | undefined > => { const items = testIdentifiers . map ( ( id ) => ( { label : idString ( 'display-reverse' , id ) , id , } ) ) ; const selected = await vscode . window . showQuickPick < RunTestPickItem > ( items , { placeHolder : 'Select a test to debug' , } ) ; return selected ?. id ; } ; let testId ; switch ( ids . length ) { case 0 : break ; case 1 : testId = ids [ 0 ] ; break ; default : testId = await selectTest ( ids ) ; if ( ! testId ) { return ; } break ; } this . debugConfigurationProvider . prepareTestRun ( typeof document === 'string' ? document : document . fileName , testId ? escapeRegExp ( idString ( 'full-name' , testId ) ) : '.*' ) ; const configs = vscode . workspace . getConfiguration ( 'launch' , this . extContext . workspace . uri ) ?. get < vscode . DebugConfiguration [ ] > ( 'configurations' ) ; let debugConfig = configs ?. find ( ( c ) => c . name === 'vscode-jest-tests.v2' ) ?? configs ?. find ( ( c ) => c . name === 'vscode-jest-tests' ) ; if ( ! debugConfig ) { this . logging ( 'debug' , 'No debug config named "vscode-jest-tests.v2" or "vscode-jest-tests" found in launch.json, will use a default config.' ) ; debugConfig = this . debugConfigurationProvider . provideDebugConfigurations ( this . extContext . workspace ) [ 0 ] ; } vscode . debug . startDebugging ( this . extContext . workspace , debugConfig ) ; } ; public runAllTests ( editor ? ) { if ( ! editor ) { if ( this . processSession . scheduleProcess ( { type : 'all-tests' } ) ) { this . dirtyFiles . clear ( ) ; return ; } } else { const name = editor . document . fileName ; let pInfo ; if ( this . testResultProvider . isTestFile ( name ) !== 'yes' ) { pInfo = this . processSession . scheduleProcess ( { type : 'by-file' , testFileName : name , notTestFile : true , } ) ; } else { pInfo = this . processSession . scheduleProcess ( { type : 'by-file-pattern' , testFileNamePattern : name , } ) ; } if ( pInfo ) { this . dirtyFiles . delete ( name ) ; return ; } } this . logging ( 'error' , 'failed to schedule the run for' , editor ?. document . fileName ) ; } onDidCloseTextDocument ( document ) { this . removeCachedTestResults ( document ) ; } removeCachedTestResults ( document , invalidateResult = false ) { if ( ! document || document . isUntitled ) { return ; } const filePath = document . fileName ; if ( invalidateResult ) { this . testResultProvider . invalidateTestResults ( filePath ) ; } else { this . testResultProvider . removeCachedResults ( filePath ) ; } } onDidChangeActiveTextEditor ( editor ) { this . triggerUpdateActiveEditor ( editor ) ; } private handleOnSaveRun ( document ) { if ( ! this . isSupportedDocument ( document ) || this . extContext . autoRun . isWatch ) { return ; } const isTestFile = this . testResultProvider . isTestFile ( document . fileName ) ; if ( this . extContext . autoRun . onSave && ( this . extContext . autoRun . onSave === 'test-src-file' || isTestFile !== 'no' ) ) { this . processSession . scheduleProcess ( { type : 'by-file' , testFileName : document . fileName , notTestFile : isTestFile !== 'yes' , } ) ; } else { this . dirtyFiles . add ( document . fileName ) ; } } param document private refreshDocumentChange ( document ? ) { for ( const editor of vscode . window . visibleTextEditors ) { if ( ( document && editor . document === document ) || vscode . workspace . getWorkspaceFolder ( editor . document . uri ) === this . extContext . workspace ) { this . triggerUpdateActiveEditor ( editor ) ; } } this . updateStatusBar ( { stats : this . toSBStats ( this . testResultProvider . getTestSuiteStats ( ) ) , } ) ; } /**
     * This event is fired with the document not dirty when:
     * - before the onDidSaveTextDocument event
     * - the document was changed by an external editor
     */ onDidChangeTextDocument ( event ) { if ( event . document . isDirty ) { return ; } if ( event . document . uri . scheme === 'git' ) { return ; } if ( event . contentChanges . length > 0 ) { return ; } this . refreshDocumentChange ( event . document ) ; } onWillSaveTextDocument ( event ) { if ( event . document . isDirty ) { this . removeCachedTestResults ( event . document , true ) ; } } onDidSaveTextDocument ( document ) { this . handleOnSaveRun ( document ) ; this . refreshDocumentChange ( document ) ; } private updateTestFileList ( ) { this . processSession . scheduleProcess ( { type : 'list-test-files' , onResult : ( files , error ) => { this . setTestFiles ( files ) ; this . logging ( 'debug' , `found ${ files ?. length } testFiles` ) ; if ( error ) { const msg = prefixWorkspace ( this . extContext , 'Failed to obtain test file list, something might not be setup right?' ) ; this . logging ( 'error' , msg , error ) ; messaging . systemWarningMessage ( msg , messaging . showTroubleshootingAction , this . setupWizardAction ( 'cmdLine' ) ) ; } } , } ) ; } onDidCreateFiles ( _event ) { this . updateTestFileList ( ) ; } onDidRenameFiles ( _event ) { this . updateTestFileList ( ) ; } onDidDeleteFiles ( _event ) { this . updateTestFileList ( ) ; } toggleCoverageOverlay ( ) { this . coverageOverlay . toggleVisibility ( ) ; this . triggerUpdateSettings ( this . extContext . settings ) ; } private setupStatusBar ( ) { this . updateStatusBar ( { state : 'initial' } ) ; } private resetStatusBar ( ) { const modes = [ ] ; if ( this . coverageOverlay . enabled ) { modes . push ( 'coverage' ) ; } modes . push ( this . extContext . autoRun . mode ) ; this . updateStatusBar ( { state : 'initial' , mode : modes , stats : emptyTestStats ( ) } ) ; } private updateStatusBar ( status ) { this . status . update ( status ) ; } _updateCoverageMap ( coverageMap ? ) { return this . coverageMapProvider . update ( coverageMap ) . then ( ( ) => { this . coverageCodeLensProvider . coverageChanged ( ) ; this . coverageOverlay . updateVisibleEditors ( ) ; } ) ; } private updateWithData ( data , process ) { const noAnsiData = resultsWithoutAnsiEscapeSequence ( data ) ; const normalizedData = resultsWithLowerCaseWindowsDriveLetters ( noAnsiData ) ; this . _updateCoverageMap ( normalizedData . coverageMap ) ; const statusList = this . testResultProvider . updateTestResults ( normalizedData , process ) ; updateDiagnostics ( statusList , this . failDiagnostics ) ; this . refreshDocumentChange ( ) ; } private generateDotsForItBlocks ( blocks , state ) { return blocks . map ( ( it ) => ( { range : new vscode . Range ( it . start . line , it . start . column , it . start . line , it . start . column + 1 ) , hoverMessage : TestResultStatusInfo [ state ] . desc , identifier : it . name , } ) ) ; } }