@ -1,18 +1,17 @@
/* global */
import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
import { SolidityUnitTesting } from '@remix-ui/solidity-unit-testing' // eslint-disable-line
import { TestTabLogic } from '@remix-ui/solidity-unit-testing' // eslint-disable-line
import { ViewPlugin } from '@remixproject/engine-web'
import { removeMultipleSlashes , removeTrailingSlashes } from '../../lib/helper'
import help er from '../../lib/helper'
import { canUseWorker , urlFromVersion } from '@remix-project/remix-solidity'
import { format } from 'util'
var yo = require ( 'yo-yo' )
var async = require ( 'async' )
var tooltip = require ( '../ui/tooltip' )
// var tooltip = require('../ui/tooltip')
var Renderer = require ( '../ui/renderer' )
var css = require ( './styles/test-tab-styles' )
var { UnitTestRunner , assertLibCode } = require ( '@remix-project/remix-tests' )
const _paq = window . _paq = window . _paq || [ ]
const TestTabLogic = require ( './testTab/testTab' )
const profile = {
name : 'solidityUnitTesting' ,
displayName : 'Solidity unit testing' ,
@ -29,33 +28,18 @@ module.exports = class TestTab extends ViewPlugin {
super ( profile )
this . compileTab = compileTab
this . contentImport = contentImport
this . _view = { el : null }
this . fileManager = fileManager
this . filePanel = filePanel
this . data = { }
this . appManager = appManager
this . renderer = new Renderer ( this )
this . testRunner = new UnitTestRunner ( )
this . hasBeenStopped = false
this . runningTestsNumber = 0
this . readyTestsNumber = 0
this . areTestsRunning = false
this . defaultPath = 'tests'
this . testTabLogic = new TestTabLogic ( this . fileManager , helper )
this . offsetToLineColumnConverter = offsetToLineColumnConverter
this . allFilesInvolved = [ '.deps/remix-tests/remix_tests.sol' , '.deps/remix-tests/remix_accounts.sol' ]
this . isDebugging = false
this . currentErrors = [ ]
appManager . event . on ( 'activate' , ( name ) => {
if ( name === 'solidity' ) this . updateRunAction ( )
} )
appManager . event . on ( 'deactivate' , ( name ) => {
if ( name === 'solidity' ) this . updateRunAction ( )
} )
this . element = document . createElement ( 'div' )
}
onActivationInternal ( ) {
this . testTabLogic = new TestTabLogic ( this . fileManager )
this . listenToEvents ( )
this . call ( 'filePanel' , 'registerContextMenuItem' , {
id : 'solidityUnitTesting' ,
@ -70,7 +54,7 @@ module.exports = class TestTab extends ViewPlugin {
async setTestFolderPath ( event ) {
if ( event . path . length > 0 ) {
await this . setCurrentPath ( event . path [ 0 ] )
this . renderComponent ( event . path [ 0 ] )
}
}
@ -93,7 +77,6 @@ module.exports = class TestTab extends ViewPlugin {
}
await this . testRunner . init ( )
await this . createTestLibs ( )
this . updateRunAction ( )
}
onDeactivation ( ) {
@ -104,26 +87,6 @@ module.exports = class TestTab extends ViewPlugin {
}
listenToEvents ( ) {
this . on ( 'filePanel' , 'newTestFileCreated' , async file => {
try {
await this . testTabLogic . getTests ( ( error , tests ) => {
if ( error ) return tooltip ( error )
this . data . allTests = tests
this . data . selectedTests = [ ... this . data . allTests ]
this . updateTestFileList ( tests )
if ( ! this . testsOutput ) return // eslint-disable-line
} )
} catch ( e ) {
console . log ( e )
this . data . allTests . push ( file )
this . data . selectedTests . push ( file )
}
} )
this . on ( 'filePanel' , 'setWorkspace' , async ( ) => {
this . setCurrentPath ( this . defaultPath )
} )
this . on ( 'filePanel' , 'workspaceCreated' , async ( ) => {
this . createTestLibs ( )
} )
@ -136,361 +99,6 @@ module.exports = class TestTab extends ViewPlugin {
this . emit ( 'compilationFinished' , source . target , source , 'soljson' , data )
}
} )
this . fileManager . events . on ( 'noFileSelected' , ( ) => {
} )
this . fileManager . events . on ( 'currentFileChanged' , ( file , provider ) => this . updateForNewCurrent ( file ) )
}
async updateForNewCurrent ( file ) {
// Ensure that when someone clicks on compilation error and that opens a new file
// Test result, which is compilation error in this case, is not cleared
if ( this . currentErrors ) {
if ( Array . isArray ( this . currentErrors ) && this . currentErrors . length > 0 ) {
const errFiles = this . currentErrors . map ( err => { if ( err . sourceLocation && err . sourceLocation . file ) return err . sourceLocation . file } )
if ( errFiles . includes ( file ) ) return
} else if ( this . currentErrors . sourceLocation && this . currentErrors . sourceLocation . file && this . currentErrors . sourceLocation . file === file ) return
}
// if current file is changed while debugging and one of the files imported in test file are opened
// do not clear the test results in SUT plugin
if ( this . isDebugging && this . allFilesInvolved . includes ( file ) ) return
this . data . allTests = [ ]
this . updateTestFileList ( )
this . clearResults ( )
this . updateGenerateFileAction ( )
if ( ! this . areTestsRunning ) this . updateRunAction ( file )
try {
await this . testTabLogic . getTests ( ( error , tests ) => {
if ( error ) return tooltip ( error )
this . data . allTests = tests
this . data . selectedTests = [ ... this . data . allTests ]
this . updateTestFileList ( tests )
if ( ! this . testsOutput ) return // eslint-disable-line
} )
} catch ( e ) {
console . log ( e )
}
}
createSingleTest ( testFile ) {
return yo `
< div class = "d-flex align-items-center py-1" >
< input class = "singleTest" id = "singleTest${testFile}" onchange = $ { ( e ) => this . toggleCheckbox ( e . target . checked , testFile ) } type = "checkbox" checked = "true" >
< label class = "singleTestLabel text-nowrap pl-2 mb-0" for = "singleTest${testFile}" > $ { testFile } < / l a b e l >
< / d i v >
`
}
listTests ( ) {
if ( ! this . data . allTests || ! this . data . allTests . length ) return [ ]
return this . data . allTests . map (
testFile => this . createSingleTest ( testFile )
)
}
toggleCheckbox ( eChecked , test ) {
if ( ! this . data . selectedTests ) {
this . data . selectedTests = this . _view . el . querySelectorAll ( '.singleTest:checked' )
}
let selectedTests = this . data . selectedTests
selectedTests = eChecked ? [ ... selectedTests , test ] : selectedTests . filter ( el => el !== test )
this . data . selectedTests = selectedTests
const checkAll = this . _view . el . querySelector ( '[id="checkAllTests"]' )
const runBtn = document . getElementById ( 'runTestsTabRunAction' )
if ( eChecked ) {
checkAll . checked = true
const stopBtnInnerText = document . getElementById ( 'runTestsTabStopAction' ) . innerText
if ( ( this . readyTestsNumber === this . runningTestsNumber || this . hasBeenStopped ) && stopBtnInnerText . trim ( ) === 'Stop' ) {
runBtn . removeAttribute ( 'disabled' )
runBtn . setAttribute ( 'title' , 'Run tests' )
}
} else if ( ! selectedTests . length ) {
checkAll . checked = false
runBtn . setAttribute ( 'disabled' , 'disabled' )
runBtn . setAttribute ( 'title' , 'No test file selected' )
}
}
checkAll ( event ) {
const checkBoxes = this . _view . el . querySelectorAll ( '.singleTest' )
const checkboxesLabels = this . _view . el . querySelectorAll ( '.singleTestLabel' )
// checks/unchecks all
for ( let i = 0 ; i < checkBoxes . length ; i ++ ) {
checkBoxes [ i ] . checked = event . target . checked
this . toggleCheckbox ( event . target . checked , checkboxesLabels [ i ] . innerText )
}
}
async discardHighlight ( ) {
await this . call ( 'editor' , 'discardHighlight' )
}
async highlightLocation ( location , runningTests , fileName ) {
if ( location ) {
var split = location . split ( ':' )
var file = split [ 2 ]
location = {
start : parseInt ( split [ 0 ] ) ,
length : parseInt ( split [ 1 ] )
}
location = this . offsetToLineColumnConverter . offsetToLineColumnWithContent (
location ,
parseInt ( file ) ,
runningTests [ fileName ] . content
)
await this . call ( 'editor' , 'discardHighlight' )
await this . call ( 'editor' , 'highlight' , location , fileName , '' , { focus : true } )
}
}
async startDebug ( txHash , web3 ) {
this . isDebugging = true
if ( ! await this . appManager . isActive ( 'debugger' ) ) await this . appManager . activatePlugin ( 'debugger' )
this . call ( 'menuicons' , 'select' , 'debugger' )
this . call ( 'debugger' , 'debug' , txHash , web3 )
}
printHHLogs ( logsArr , testName ) {
let finalLogs = ` <b> ${ testName } :</b> \n `
for ( const log of logsArr ) {
let formattedLog
// Hardhat implements the same formatting options that can be found in Node.js' console.log,
// which in turn uses util.format: https://nodejs.org/dist/latest-v12.x/docs/api/util.html#util_util_format_format_args
// For example: console.log("Name: %s, Age: %d", remix, 6) will log 'Name: remix, Age: 6'
// We check first arg to determine if 'util.format' is needed
if ( typeof log [ 0 ] === 'string' && ( log [ 0 ] . includes ( '%s' ) || log [ 0 ] . includes ( '%d' ) ) ) {
formattedLog = format ( log [ 0 ] , ... log . slice ( 1 ) )
} else {
formattedLog = log . join ( ' ' )
}
finalLogs = finalLogs + ' ' + formattedLog + '\n'
}
_paq . push ( [ 'trackEvent' , 'solidityUnitTesting' , 'hardhat' , 'console.log' ] )
this . call ( 'terminal' , 'log' , { type : 'info' , value : finalLogs } )
}
testCallback ( result , runningTests ) {
this . testsOutput . hidden = false
let debugBtn = yo ` `
if ( ( result . type === 'testPass' || result . type === 'testFailure' ) && result . debugTxHash ) {
const { web3 , debugTxHash } = result
debugBtn = yo ` <div id= ${ result . value . replaceAll ( ' ' , '_' ) } class="btn border btn btn-sm ml-1" title="Start debugging" onclick= ${ ( ) => this . startDebug ( debugTxHash , web3 ) } >
< i class = "fas fa-bug" > < / i >
< / d i v > `
debugBtn . style . cursor = 'pointer'
}
if ( result . type === 'contract' ) {
this . testSuite = result . value
if ( this . testSuites ) {
this . testSuites . push ( this . testSuite )
} else {
this . testSuites = [ this . testSuite ]
}
this . rawFileName = result . filename
this . runningTestFileName = this . cleanFileName ( this . rawFileName , this . testSuite )
this . outputHeader = yo `
< div id = "${this.runningTestFileName}" data - id = "testTabSolidityUnitTestsOutputheader" class = "pt-1" >
< span class = "font-weight-bold" > $ { this . testSuite } ( $ { this . rawFileName } ) < / s p a n >
< / d i v >
`
this . testsOutput . appendChild ( this . outputHeader )
} else if ( result . type === 'testPass' ) {
if ( result . hhLogs && result . hhLogs . length ) this . printHHLogs ( result . hhLogs , result . value )
this . testsOutput . appendChild ( yo `
< div
id = "${this.runningTestFileName}"
data - id = "testTabSolidityUnitTestsOutputheader"
class = "${css.testPass} ${css.testLog} bg-light mb-2 px-2 text-success border-0"
onclick = $ { ( ) => this . discardHighlight ( ) }
>
< div class = "d-flex my-1 align-items-start justify-content-between" >
< span style = "margin-block: auto" > ✓ $ { result . value } < / s p a n >
$ { debugBtn }
< / d i v >
< / d i v >
` )
} else if ( result . type === 'testFailure' ) {
if ( result . hhLogs && result . hhLogs . length ) this . printHHLogs ( result . hhLogs , result . value )
if ( ! result . assertMethod ) {
this . testsOutput . appendChild ( yo `
< div
class = "bg-light mb-2 px-2 ${css.testLog} d-flex flex-column text-danger border-0"
id = "UTContext${result.context}"
onclick = $ { ( ) => this . highlightLocation ( result . location , runningTests , result . filename ) }
>
< div class = "d-flex my-1 align-items-start justify-content-between" >
< span > ✘ $ { result . value } < / s p a n >
$ { debugBtn }
< / d i v >
< span class = "text-dark" > Error Message : < / s p a n >
< span class = "pb-2 text-break" > "${result.errMsg}" < / s p a n >
< / d i v >
` )
} else {
const preposition = result . assertMethod === 'equal' || result . assertMethod === 'notEqual' ? 'to' : ''
const method = result . assertMethod === 'ok' ? '' : result . assertMethod
const expected = result . assertMethod === 'ok' ? '\'true\'' : result . expected
this . testsOutput . appendChild ( yo `
< div
class = "bg-light mb-2 px-2 ${css.testLog} d-flex flex-column text-danger border-0"
id = "UTContext${result.context}"
onclick = $ { ( ) => this . highlightLocation ( result . location , runningTests , result . filename ) }
>
< div class = "d-flex my-1 align-items-start justify-content-between" >
< span > ✘ $ { result . value } < / s p a n >
$ { debugBtn }
< / d i v >
< span class = "text-dark" > Error Message : < / s p a n >
< span class = "pb-2 text-break" > "${result.errMsg}" < / s p a n >
< span class = "text-dark" > Assertion : < / s p a n >
< div class = "d-flex flex-wrap" >
< span > Expected value should be < / s p a n >
< div class = "mx-1 font-weight-bold" > $ { method } < / d i v >
< div > $ { preposition } $ { expected } < / d i v >
< / d i v >
< span class = "text-dark" > Received value : < / s p a n >
< span > $ { result . returned } < / s p a n >
< span class = "text-dark text-sm pb-2" > Skipping the remaining tests of the function . < / s p a n >
< / d i v >
` )
}
} else if ( result . type === 'logOnly' ) {
if ( result . hhLogs && result . hhLogs . length ) this . printHHLogs ( result . hhLogs , result . value )
}
}
resultsCallback ( _err , result , cb ) {
// total stats for the test
// result.passingNum
// result.failureNum
// result.timePassed
cb ( )
}
cleanFileName ( fileName , testSuite ) {
return fileName ? fileName . replace ( /\//g , '_' ) . replace ( /\./g , '_' ) + testSuite : fileName
}
setHeader ( status ) {
if ( status ) {
const label = yo `
< div
class = "alert-success d-inline-block mb-1 mr-1 p-1 passed_${this.runningTestFileName}"
title = "All contract tests passed"
>
PASS
< / d i v >
`
this . outputHeader && yo . update ( this . outputHeader , yo `
< div id = "${this.runningTestFileName}" data - id = "testTabSolidityUnitTestsOutputheader" class = "pt-1" >
$ { label } < span class = "font-weight-bold" > $ { this . testSuite } ( $ { this . rawFileName } ) < / s p a n >
< / d i v >
` )
} else {
const label = yo `
< div
class = "alert-danger d-inline-block mb-1 mr-1 p-1 failed_${this.runningTestFileName}"
title = "At least one contract test failed"
>
FAIL
< / d i v >
`
this . outputHeader && yo . update ( this . outputHeader , yo `
< div id = "${this.runningTestFileName}" data - id = "testTabSolidityUnitTestsOutputheader" class = "pt-1" >
$ { label } < span class = "font-weight-bold" > $ { this . testSuite } ( $ { this . rawFileName } ) < / s p a n >
< / d i v >
` )
}
}
updateFinalResult ( _errors , result , filename ) {
++ this . readyTestsNumber
this . testsOutput . hidden = false
if ( ! result && ( _errors && ( _errors . errors || ( Array . isArray ( _errors ) && ( _errors [ 0 ] . message || _errors [ 0 ] . formattedMessage ) ) ) ) ) {
this . testCallback ( { type : 'contract' , filename } )
this . currentErrors = _errors . errors
this . setHeader ( false )
}
if ( _errors && _errors . errors ) {
_errors . errors . forEach ( ( err ) => this . renderer . error ( err . formattedMessage || err . message , this . testsOutput , { type : err . severity , errorType : err . type } ) )
} else if ( _errors && Array . isArray ( _errors ) && ( _errors [ 0 ] . message || _errors [ 0 ] . formattedMessage ) ) {
_errors . forEach ( ( err ) => this . renderer . error ( err . formattedMessage || err . message , this . testsOutput , { type : err . severity , errorType : err . type } ) )
} else if ( _errors && ! _errors . errors && ! Array . isArray ( _errors ) ) {
// To track error like this: https://github.com/ethereum/remix/pull/1438
this . renderer . error ( _errors . formattedMessage || _errors . message , this . testsOutput , { type : 'error' } )
}
yo . update ( this . resultStatistics , this . createResultLabel ( ) )
if ( result ) {
const totalTime = parseFloat ( result . totalTime ) . toFixed ( 2 )
if ( result . totalPassing > 0 && result . totalFailing > 0 ) {
this . testsOutput . appendChild ( yo `
< div class = "d-flex alert-secondary mb-3 p-3 flex-column" >
< span class = "font-weight-bold" > Result for $ { filename } < / s p a n >
< span class = "text-success" > Passing : $ { result . totalPassing } < / s p a n >
< span class = "text-danger" > Failing : $ { result . totalFailing } < / s p a n >
< span > Total time : $ { totalTime } s < / s p a n >
< / d i v >
` )
} else if ( result . totalPassing > 0 && result . totalFailing <= 0 ) {
this . testsOutput . appendChild ( yo `
< div class = "d-flex alert-secondary mb-3 p-3 flex-column" >
< span class = "font-weight-bold" > Result for $ { filename } < / s p a n >
< span class = "text-success" > Passing : $ { result . totalPassing } < / s p a n >
< span > Total time : $ { totalTime } s < / s p a n >
< / d i v >
` )
} else if ( result . totalPassing <= 0 && result . totalFailing > 0 ) {
this . testsOutput . appendChild ( yo `
< div class = "d-flex alert-secondary mb-3 p-3 flex-column" >
< span class = "font-weight-bold" > Result for $ { filename } < / s p a n >
< span class = "text-danger" > Failing : $ { result . totalFailing } < / s p a n >
< span > Total time : $ { totalTime } s < / s p a n >
< / d i v >
` )
}
// fix for displaying right label for multiple tests (testsuites) in a single file
this . testSuites . forEach ( testSuite => {
this . testSuite = testSuite
this . runningTestFileName = this . cleanFileName ( filename , this . testSuite )
this . outputHeader = document . querySelector ( ` # ${ this . runningTestFileName } ` )
this . setHeader ( true )
} )
result . errors . forEach ( ( error , index ) => {
this . testSuite = error . context
this . runningTestFileName = this . cleanFileName ( filename , error . context )
this . outputHeader = document . querySelector ( ` # ${ this . runningTestFileName } ` )
const isFailingLabel = document . querySelector ( ` .failed_ ${ this . runningTestFileName } ` )
if ( ! isFailingLabel ) this . setHeader ( false )
} )
this . testsOutput . appendChild ( yo `
< div >
< p class = "text-info mb-2 border-top m-0" > < / p >
< / d i v >
` )
}
if ( this . hasBeenStopped && ( this . readyTestsNumber !== this . runningTestsNumber ) ) {
// if all tests has been through before stopping no need to print this.
this . testsExecutionStopped . hidden = false
}
if ( _errors ) this . testsExecutionStoppedError . hidden = false
if ( _errors || this . hasBeenStopped || this . readyTestsNumber === this . runningTestsNumber ) {
// All tests are ready or the operation has been canceled or there was a compilation error in one of the test files.
const stopBtn = document . getElementById ( 'runTestsTabStopAction' )
stopBtn . setAttribute ( 'disabled' , 'disabled' )
const stopBtnLabel = document . getElementById ( 'runTestsTabStopActionLabel' )
stopBtnLabel . innerText = 'Stop'
if ( this . data . selectedTests . length !== 0 ) {
const runBtn = document . getElementById ( 'runTestsTabRunAction' )
runBtn . removeAttribute ( 'disabled' )
}
this . areTestsRunning = false
}
}
async testFromPath ( path ) {
@ -498,17 +106,6 @@ module.exports = class TestTab extends ViewPlugin {
return this . testFromSource ( fileContent , path )
}
/ * *
* Changes the current path of Unit Testing Plugin
* @ param path - the path from where UT plugin takes _test . sol files to run
* /
async setCurrentPath ( path ) {
this . testTabLogic . setCurrentPath ( path )
this . inputPath . value = path
this . updateDirList ( path )
await this . updateForNewCurrent ( )
}
/ *
Test is not associated with the UI
* /
@ -534,337 +131,15 @@ module.exports = class TestTab extends ViewPlugin {
} )
}
runTest ( testFilePath , callback ) {
this . isDebugging = false
if ( this . hasBeenStopped ) {
this . updateFinalResult ( )
return
}
this . resultStatistics . hidden = false
this . fileManager . readFile ( testFilePath ) . then ( ( content ) => {
const runningTests = { }
runningTests [ testFilePath ] = { content }
const { currentVersion , evmVersion , optimize , runs , isUrl } = this . compileTab . getCurrentCompilerConfig ( )
const currentCompilerUrl = isUrl ? currentVersion : urlFromVersion ( currentVersion )
const compilerConfig = {
currentCompilerUrl ,
evmVersion ,
optimize ,
usingWorker : canUseWorker ( currentVersion ) ,
runs
}
const deployCb = async ( file , contractAddress ) => {
const compilerData = await this . call ( 'compilerArtefacts' , 'getCompilerAbstract' , file )
await this . call ( 'compilerArtefacts' , 'addResolvedContract' , contractAddress , compilerData )
}
this . testRunner . runTestSources (
runningTests ,
compilerConfig ,
( result ) => this . testCallback ( result , runningTests ) ,
( _err , result , cb ) => this . resultsCallback ( _err , result , cb ) ,
deployCb ,
( error , result ) => {
this . updateFinalResult ( error , result , testFilePath )
callback ( error )
} , ( url , cb ) => {
return this . contentImport . resolveAndSave ( url ) . then ( ( result ) => cb ( null , result ) ) . catch ( ( error ) => cb ( error . message ) )
} , { testFilePath }
)
} ) . catch ( ( error ) => {
if ( error ) return // eslint-disable-line
} )
}
handleCreateFolder ( ) {
this . inputPath . value = this . trimTestDirInput ( this . inputPath . value )
let path = removeMultipleSlashes ( this . inputPath . value )
if ( path !== '/' ) path = removeTrailingSlashes ( path )
if ( this . inputPath . value === '' ) this . inputPath . value = this . defaultPath
this . inputPath . value = path
this . testTabLogic . generateTestFolder ( this . inputPath . value )
this . createTestFolder . disabled = true
this . updateGenerateFileAction ( ) . disabled = false
this . testTabLogic . setCurrentPath ( this . inputPath . value )
this . updateRunAction ( )
this . updateForNewCurrent ( )
this . uiPathList . appendChild ( yo ` <option> ${ this . inputPath . value } </option> ` )
}
clearResults ( ) {
yo . update ( this . resultStatistics , yo ` <span></span> ` )
this . call ( 'editor' , 'clearAnnotations' )
this . testsOutput . innerHTML = ''
this . testsOutput . hidden = true
this . testsExecutionStopped . hidden = true
this . testsExecutionStoppedError . hidden = true
}
runTests ( ) {
this . areTestsRunning = true
this . hasBeenStopped = false
this . readyTestsNumber = 0
this . runningTestsNumber = this . data . selectedTests . length
const stopBtn = document . getElementById ( 'runTestsTabStopAction' )
stopBtn . removeAttribute ( 'disabled' )
const runBtn = document . getElementById ( 'runTestsTabRunAction' )
runBtn . setAttribute ( 'disabled' , 'disabled' )
this . clearResults ( )
yo . update ( this . resultStatistics , this . createResultLabel ( ) )
const tests = this . data . selectedTests
if ( ! tests ) return
this . resultStatistics . hidden = tests . length === 0
_paq . push ( [ 'trackEvent' , 'solidityUnitTesting' , 'runTests' ] )
async . eachOfSeries ( tests , ( value , key , callback ) => {
if ( this . hasBeenStopped ) return
this . runTest ( value , callback )
} )
}
stopTests ( ) {
this . hasBeenStopped = true
const stopBtnLabel = document . getElementById ( 'runTestsTabStopActionLabel' )
stopBtnLabel . innerText = 'Stopping'
const stopBtn = document . getElementById ( 'runTestsTabStopAction' )
stopBtn . setAttribute ( 'disabled' , 'disabled' )
const runBtn = document . getElementById ( 'runTestsTabRunAction' )
runBtn . setAttribute ( 'disabled' , 'disabled' )
}
updateGenerateFileAction ( ) {
const el = yo `
< button
class = "btn border w-50"
data - id = "testTabGenerateTestFile"
title = "Generate sample test file."
onclick = "${this.testTabLogic.generateTestFile.bind(this.testTabLogic)}"
>
Generate
< / b u t t o n >
`
if ( ! this . generateFileActionElement ) {
this . generateFileActionElement = el
} else {
yo . update ( this . generateFileActionElement , el )
}
return this . generateFileActionElement
}
updateRunAction ( currentFile ) {
const el = yo `
< button id = "runTestsTabRunAction" title = "Run tests" data - id = "testTabRunTestsTabRunAction" class = "w-50 btn btn-primary" onclick = "${() => this.runTests()}" >
< span class = "fas fa-play ml-2" > < / s p a n >
< label class = "${css.labelOnBtn} btn btn-primary p-1 ml-2 m-0" > Run < / l a b e l >
< / b u t t o n >
`
const isSolidityActive = this . appManager . isActive ( 'solidity' )
if ( ! isSolidityActive || ! this . listTests ( ) . length ) {
el . setAttribute ( 'disabled' , 'disabled' )
if ( ! currentFile || ( currentFile && currentFile . split ( '.' ) . pop ( ) . toLowerCase ( ) !== 'sol' ) ) {
el . setAttribute ( 'title' , 'No solidity file selected' )
} else {
el . setAttribute ( 'title' , 'The "Solidity Plugin" should be activated' )
}
}
if ( ! this . runActionElement ) {
this . runActionElement = el
} else {
yo . update ( this . runActionElement , el )
}
return this . runActionElement
}
updateStopAction ( ) {
return yo `
< button id = "runTestsTabStopAction" data - id = "testTabRunTestsTabStopAction" class = "w-50 pl-2 ml-2 btn btn-secondary" disabled = "disabled" title = "Stop running tests" onclick = $ { ( ) => this . stopTests ( ) } " >
< span class = "fas fa-stop ml-2" > < / s p a n >
< label class = "${css.labelOnBtn} btn btn-secondary p-1 ml-2 m-0" id = "runTestsTabStopActionLabel" > Stop < / l a b e l >
< / b u t t o n >
`
}
updateTestFileList ( tests ) {
const testsMessage = ( tests && tests . length ? this . listTests ( ) : 'No test file available' )
const el = yo ` <div class=" ${ css . testList } py-2 mt-0 border-bottom"> ${ testsMessage } </div> `
if ( ! this . testFilesListElement ) {
this . testFilesListElement = el
} else {
yo . update ( this . testFilesListElement , el )
}
this . updateRunAction ( )
return this . testFilesListElement
}
selectAll ( ) {
return yo `
< div class = "d-flex align-items-center mx-3 pb-2 mt-2 border-bottom" >
< input id = "checkAllTests"
type = "checkbox"
data - id = "testTabCheckAllTests"
onclick = "${(event) => { this.checkAll(event) }}"
checked = "true"
>
< label class = "text-nowrap pl-2 mb-0" for = "checkAllTests" > Select all < / l a b e l >
< / d i v >
`
}
infoButton ( ) {
return yo `
< a class = "btn border text-decoration-none pr-0 d-flex w-50 ml-2" title = "Check out documentation." target = "__blank" href = "https://remix-ide.readthedocs.io/en/latest/unittesting.html#test-directory" >
< label class = "btn p-1 ml-2 m-0" > How to use ... < / l a b e l >
< / a >
`
}
createResultLabel ( ) {
if ( ! this . data . selectedTests ) return yo ` <span></span> `
const ready = this . readyTestsNumber ? ` ${ this . readyTestsNumber } ` : '0'
return yo ` <span class='text-info h6'>Progress: ${ ready } finished (of ${ this . runningTestsNumber } )</span> `
}
updateDirList ( path ) {
for ( const o of this . uiPathList . querySelectorAll ( 'option' ) ) o . remove ( )
this . testTabLogic . dirList ( path ) . then ( ( options ) => {
options . forEach ( ( path ) => this . uiPathList . appendChild ( yo ` <option> ${ path } </option> ` ) )
} )
}
trimTestDirInput ( input ) {
if ( input . includes ( '/' ) ) return input . split ( '/' ) . map ( e => e . trim ( ) ) . join ( '/' )
else return input . trim ( )
}
pathAdded ( text ) {
for ( const option of this . uiPathList . querySelectorAll ( 'option' ) ) {
if ( option . innerHTML === text ) return true
}
return false
}
async handleTestDirInput ( e ) {
let testDirInput = this . trimTestDirInput ( this . inputPath . value )
testDirInput = removeMultipleSlashes ( testDirInput )
if ( testDirInput !== '/' ) testDirInput = removeTrailingSlashes ( testDirInput )
if ( e . key === 'Enter' ) {
this . inputPath . value = testDirInput
if ( await this . testTabLogic . pathExists ( testDirInput ) ) {
this . testTabLogic . setCurrentPath ( testDirInput )
this . updateForNewCurrent ( )
return
}
}
if ( testDirInput ) {
if ( testDirInput . endsWith ( '/' ) && testDirInput !== '/' ) {
testDirInput = removeTrailingSlashes ( testDirInput )
if ( this . testTabLogic . currentPath === testDirInput . substr ( 0 , testDirInput . length - 1 ) ) {
this . createTestFolder . disabled = true
this . updateGenerateFileAction ( ) . disabled = true
}
this . updateDirList ( testDirInput )
} else {
// If there is no matching folder in the workspace with entered text, enable Create button
if ( await this . testTabLogic . pathExists ( testDirInput ) ) {
this . createTestFolder . disabled = true
this . updateGenerateFileAction ( ) . disabled = false
} else {
// Enable Create button
this . createTestFolder . disabled = false
// Disable Generate button because dir does not exist
this . updateGenerateFileAction ( ) . disabled = true
}
}
} else {
this . updateDirList ( '/' )
}
}
async handleEnter ( e ) {
this . inputPath . value = removeMultipleSlashes ( this . trimTestDirInput ( this . inputPath . value ) )
if ( this . createTestFolder . disabled ) {
if ( await this . testTabLogic . pathExists ( this . inputPath . value ) ) {
this . testTabLogic . setCurrentPath ( this . inputPath . value )
this . updateForNewCurrent ( )
}
}
}
render ( ) {
this . onActivationInternal ( )
this . testsOutput = yo ` <div class="mx-3 mb-2 pb-4 border-top border-primary" hidden='true' id="solidityUnittestsOutput" data-id="testTabSolidityUnitTestsOutput"></a> `
this . testsExecutionStopped = yo ` <label class="text-warning h6" data-id="testTabTestsExecutionStopped">The test execution has been stopped</label> `
this . testsExecutionStoppedError = yo ` <label class="text-danger h6" data-id="testTabTestsExecutionStoppedError">The test execution has been stopped because of error(s) in your test file</label> `
this . uiPathList = yo ` <datalist id="utPathList"></datalist> `
this . inputPath = yo ` <input
placeholder = $ { this . defaultPath }
list = "utPathList"
class = "${css.inputFolder} custom-select"
id = "utPath"
data - id = "uiPathInput"
name = "utPath"
title = "Press 'Enter' to change the path for test files."
style = "background-image: var(--primary);"
onkeyup = $ { ( e ) => this . handleTestDirInput ( e ) }
onchange = $ { async ( e ) => this . handleEnter ( e ) }
/ > `
this . createTestFolder = yo `
< button
class = "btn border ml-2"
data - id = "testTabGenerateTestFolder"
title = "Create a test folder"
disabled = true
onclick = $ { ( e ) => this . handleCreateFolder ( ) }
>
Create
< / b u t t o n >
`
this . renderComponent ( 'tests' )
return this . element
}
const availablePaths = yo `
< div >
< div class = "d-flex p-2" >
$ { this . inputPath }
$ { this . createTestFolder }
$ { this . uiPathList }
< / d i v >
< / d i v >
`
this . updateDirList ( '/' )
this . testsExecutionStopped . hidden = true
this . testsExecutionStoppedError . hidden = true
this . resultStatistics = this . createResultLabel ( )
this . resultStatistics . hidden = true
const el = yo `
< div class = "${css.testTabView} px-2" id = "testView" >
< div class = "${css.infoBox}" >
< p class = "text-lg" > Test your smart contract in Solidity . < / p >
< p > Select directory to load and generate test files . < / p >
< label > Test directory : < / l a b e l >
$ { availablePaths }
< / d i v >
< div class = "${css.tests}" >
< div class = "d-flex p-2" >
$ { this . updateGenerateFileAction ( ) }
$ { this . infoButton ( ) }
< / d i v >
< div class = "d-flex p-2" >
$ { this . updateRunAction ( ) }
$ { this . updateStopAction ( ) }
< / d i v >
$ { this . selectAll ( ) }
$ { this . updateTestFileList ( ) }
< div class = "align-items-start flex-column mt-2 mx-3 mb-0" >
$ { this . resultStatistics }
$ { this . testsExecutionStopped }
$ { this . testsExecutionStoppedError }
< / d i v >
$ { this . testsOutput }
< / d i v >
< / d i v >
`
this . _view . el = el
this . testTabLogic . setCurrentPath ( this . defaultPath )
this . updateForNewCurrent ( this . fileManager . currentFile ( ) )
return el
renderComponent ( testDirPath ) {
ReactDOM . render (
< SolidityUnitTesting testTab = { this } helper = { helper } initialPath = { testDirPath } / >
, this . element )
}
}