import { Indexer } from '@salesforce/lightning-lsp-common' ; import { parse } from 'jest-editor-support' ; import * as vscode from 'vscode' ; import { LwcJestTestResults , RawTestResult , TestCaseInfo , TestFileInfo , TestInfoKind , TestResultStatus , TestType } from '../types' ; import { LWC_TEST_GLOB_PATTERN } from '../types/constants' ; import { extractPositionFromFailureMessage , IExtendedParseResults , ItBlockWithAncestorTitles , populateAncestorTitles , sanitizeFailureMessage } from './jestUtils' ; class LwcTestIndexer implements Indexer , vscode . Disposable { private disposables : vscode . Disposable [ ] = [ ] ; private hasIndexedTestFiles = false ; private testFileInfoMap = new Map < string , TestFileInfo > ( ) ; private diagnosticCollection = vscode . languages . createDiagnosticCollection ( 'lwcTestErrors' ) ; private onDidUpdateTestResultsIndexEventEmitter = new vscode . EventEmitter < undefined > ( ) ; private onDidUpdateTestIndexEventEmitter = new vscode . EventEmitter < undefined > ( ) ; public onDidUpdateTestResultsIndex = this . onDidUpdateTestResultsIndexEventEmitter . event ; public onDidUpdateTestIndex = this . onDidUpdateTestIndexEventEmitter . event ; param context public register ( context ) { context . subscriptions . push ( this ) ; this . configureAndIndex ( ) . catch ( error => console . error ( error ) ) ; } public dispose ( ) { while ( this . disposables . length ) { const disposable = this . disposables . pop ( ) ; if ( disposable ) { disposable . dispose ( ) ; } } } /**
     * Set up file system watcher for test files change/create/delete.
     */ public async configureAndIndex ( ) { const lwcTestWatcher = vscode . workspace . createFileSystemWatcher ( LWC_TEST_GLOB_PATTERN ) ; lwcTestWatcher . onDidCreate ( async ( testUri ) => { await this . indexTestCases ( testUri ) ; this . onDidUpdateTestIndexEventEmitter . fire ( undefined ) ; } , this , this . disposables ) ; lwcTestWatcher . onDidChange ( async ( testUri ) => { await this . indexTestCases ( testUri ) ; this . onDidUpdateTestIndexEventEmitter . fire ( undefined ) ; } , this , this . disposables ) ; lwcTestWatcher . onDidDelete ( testUri => { const { fsPath } = testUri ; this . resetTestFileIndex ( fsPath ) ; this . onDidUpdateTestIndexEventEmitter . fire ( undefined ) ; } , this , this . disposables ) ; } /**
     * Reset test indexer
     */ public resetIndex ( ) { this . hasIndexedTestFiles = false ; this . testFileInfoMap . clear ( ) ; this . diagnosticCollection . clear ( ) ; this . onDidUpdateTestIndexEventEmitter . fire ( undefined ) ; } /**
     * Find test files in the workspace if needed.
     * It lazily index all test files until opening test explorer
     */ public async findAllTestFileInfo ( ) { if ( this . hasIndexedTestFiles ) { return [ ... this . testFileInfoMap . values ( ) ] ; } return await this . indexAllTestFiles ( ) ; } public async indexTestCases ( testUri ) { const { fsPath : testFsPath } = testUri ; let testFileInfo = this . testFileInfoMap . get ( testFsPath ) ; if ( ! testFileInfo ) { testFileInfo = this . indexTestFile ( testFsPath ) ; } return this . parseTestFileAndMergeTestResults ( testFileInfo ) ; } param testUri public async findTestInfoFromLwcJestTestFile ( testUri ) { const { fsPath : testFsPath } = testUri ; let testFileInfo = this . testFileInfoMap . get ( testFsPath ) ; if ( ! testFileInfo ) { testFileInfo = this . indexTestFile ( testFsPath ) ; } if ( testFileInfo . testCasesInfo ) { return testFileInfo . testCasesInfo ; } return this . parseTestFileAndMergeTestResults ( testFileInfo ) ; } private parseTestFileAndMergeTestResults ( testFileInfo ) { try { const { testUri } = testFileInfo ; const { fsPath : testFsPath } = testUri ; const parseResults = parse ( testFsPath ) as IExtendedParseResults ; populateAncestorTitles ( parseResults ) ; const itBlocks = ( parseResults . itBlocksWithAncestorTitles || parseResults . itBlocks ) as ItBlockWithAncestorTitles [ ] ; const testCasesInfo = itBlocks . map ( itBlock => { const { name , nameRange , ancestorTitles } = itBlock ; const testName = name ; const testRange = new vscode . Range ( new vscode . Position ( nameRange . start . line - 1 , nameRange . start . column - 1 ) , new vscode . Position ( nameRange . end . line - 1 , nameRange . end . column ) ) ; const testLocation = new vscode . Location ( testUri , testRange ) ; const testCaseInfo = { kind : TestInfoKind . TEST_CASE , testType : TestType . LWC , testName , testUri , testLocation , ancestorTitles } ; return testCaseInfo ; } ) ; if ( testFileInfo . rawTestResults ) { this . mergeTestResults ( testCasesInfo , testFileInfo . rawTestResults ) ; } testFileInfo . testCasesInfo = testCasesInfo ; return testCasesInfo ; } catch ( error ) { console . error ( error ) ; testFileInfo . testCasesInfo = [ ] ; return [ ] ; } } /**
     * Find all LWC test files in the workspace by glob pattern.
     * This does not start parsing the test files.
     */ private async indexAllTestFiles ( ) { const lwcJestTestFiles = await vscode . workspace . findFiles ( LWC_TEST_GLOB_PATTERN , '**/node_modules/**' ) ; const allTestFileInfo = lwcJestTestFiles . map ( lwcJestTestFile => { const { fsPath } = lwcJestTestFile ; let testFileInfo = this . testFileInfoMap . get ( fsPath ) ; if ( ! testFileInfo ) { testFileInfo = this . indexTestFile ( fsPath ) ; } return testFileInfo ; } ) ; this . hasIndexedTestFiles = true ; return allTestFileInfo ; } private indexTestFile ( testFsPath ) { const testUri = vscode . Uri . file ( testFsPath ) ; const testLocation = new vscode . Location ( testUri , new vscode . Position ( 0 , 0 ) ) ; const testFileInfo = { kind : TestInfoKind . TEST_FILE , testType : TestType . LWC , testUri , testLocation } ; this . testFileInfoMap . set ( testFsPath , testFileInfo ) ; return testFileInfo ; } private resetTestFileIndex ( testFsPath ) { this . testFileInfoMap . delete ( testFsPath ) ; } private mergeTestResults ( testCasesInfo , rawTestResults ) { const rawTestResultsByTitle = new Map < string , RawTestResult [ ] > ( ) ; rawTestResults . forEach ( rawTestResult => { const { title } = rawTestResult ; rawTestResultsByTitle . set ( title , [ ... ( rawTestResultsByTitle . get ( title ) || [ ] ) , rawTestResult ] ) ; } ) ; testCasesInfo . forEach ( testCaseInfo => { const { testName , ancestorTitles : testCaseAncestorTitles } = testCaseInfo ; const rawTestResultsOfTestName = rawTestResultsByTitle . get ( testName ) ; if ( rawTestResultsOfTestName ) { const matchedRawTestResults = rawTestResultsOfTestName . filter ( rawTestResultOfTestName => { const { title , ancestorTitles } = rawTestResultOfTestName ; const isMatched = testCaseAncestorTitles ? testName === title && JSON . stringify ( testCaseAncestorTitles ) === JSON . stringify ( ancestorTitles ) : testName === title ; return isMatched ; } ) ; if ( matchedRawTestResults && matchedRawTestResults . length > 0 ) { testCaseInfo . testResult = { status : matchedRawTestResults [ 0 ] . status } ; } } } ) ; } param testResults public updateTestResults ( testResults ) { testResults . testResults . forEach ( testResult => { const { name , status : testFileStatus , assertionResults } = testResult ; const testFsPath = vscode . Uri . file ( name ) . fsPath ; let testFileInfo = this . testFileInfoMap . get ( testFsPath ) ; if ( ! testFileInfo ) { testFileInfo = this . indexTestFile ( testFsPath ) ; } let testFileResultStatus = TestResultStatus . UNKNOWN ; if ( testFileStatus === 'passed' ) { testFileResultStatus = TestResultStatus . PASSED ; } else if ( testFileStatus === 'failed' ) { testFileResultStatus = TestResultStatus . FAILED ; } testFileInfo . testResult = { status : testFileResultStatus } ; const testUri = vscode . Uri . file ( testFsPath ) ; const diagnostics = assertionResults . reduce ( ( diagnosticsResult , assertionResult ) => { const { failureMessages , location } = assertionResult ; if ( failureMessages && failureMessages . length > 0 ) { const failureMessage = sanitizeFailureMessage ( failureMessages [ 0 ] ) ; const failurePosition = extractPositionFromFailureMessage ( testFsPath , failureMessage ) || new vscode . Position ( location . line - 1 , location . column - 1 ) ; const diagnostic = new vscode . Diagnostic ( new vscode . Range ( failurePosition , failurePosition ) , failureMessage ) ; diagnosticsResult . push ( diagnostic ) ; } return diagnosticsResult ; } , [ ] ) ; this . diagnosticCollection . set ( testUri , diagnostics ) ; const rawTestResults = assertionResults . map ( assertionResult => { const { title , status , ancestorTitles } = assertionResult ; let testResultStatus ; if ( status === 'passed' ) { testResultStatus = TestResultStatus . PASSED ; } else if ( status === 'failed' ) { testResultStatus = TestResultStatus . FAILED ; } else { testResultStatus = TestResultStatus . SKIPPED ; } const testCaseInfo = { title , status : testResultStatus , ancestorTitles } ; return testCaseInfo ; } ) ; testFileInfo . rawTestResults = rawTestResults ; const testCasesInfo = testFileInfo . testCasesInfo ; if ( testCasesInfo ) { this . mergeTestResults ( testCasesInfo , rawTestResults ) ; } } ) ; this . onDidUpdateTestResultsIndexEventEmitter . fire ( undefined ) ; } } export const lwcTestIndexer = new LwcTestIndexer ( ) ;