remix-ide
Version:
Minimalistic browser-based Solidity IDE
255 lines (233 loc) • 11 kB
JavaScript
var yo = require('yo-yo')
var async = require('async')
var tooltip = require('../ui/tooltip')
var css = require('./styles/test-tab-styles')
var remixTests = require('remix-tests')
import { BaseApi } from 'remix-plugin'
const TestTabLogic = require('./testTab/testTab')
const profile = {
name: 'solidityUnitTesting',
displayName: 'Solidity unit testing',
methods: [],
events: [],
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4wUDEhQZ0zbrmQAAAfNJREFUWMPF17lrFVEUx/EPaIKovfAScSndjUtULFQSYhHF0r/Dwsa/RywUiTaWgvaChWsiKkSMZte4o7G5A49x7r0zLy/PA6eZOef3PXebuYfu2xCmcQ9b9NgOYw6rwR9ia6/gR7HQBi/8PjavN/w4FivghV9bT/gwlhLwHzjTVPQ8rqAvE3ciA/+O8abwy/gVBG4lijiJ5czIL64FXvhNbCzFnaoBv9AUPo7fEcEb2BDiTuNTAv4NYxX6u/EIM7GZuZoQXcX1sJk+J2K+YrRCexfetsX9xKVyUB9uZ4r4k3j3BSMR+JvIMv2zQfsxkSkiBj9XAd8ZgRf+vmop+nGnAXwlcs534HUm93FsQ9YtIjby7XiVyZ3BntSpyBWxgrMR+FQG/gF76xzNftxtMO1rgo+G5AdBqLBN4d9eCCyHD1En8Oi0j4UPSBE4hcFSERN4Fz7BZRvEZKcjHynBC5/EQI1lGqgJ3xcTmE4kvswUMRBiUvCPKTg8zQi8QKsirxXe5eD7c1N4ALMZoeelIlrhWSpnNmjXsoM1iihmYhueZGIXcKTp7/hQ6UZb5c+Cp2LmglZHVqeIlC+G2/GarNMiFnGsWzfdpkV0Fd7e5czXgC+FvmDdWq35/wVvbzbnI/DhXvV9Q6W+r6fw9hZsKnjX4H8B0Aamri7CrBsAAAAASUVORK5CYII=',
description: 'Fast tool to generate unit tests for your contracts',
location: 'sidePanel',
documentation: 'https://remix-ide.readthedocs.io/en/latest/unittesting.html'
}
module.exports = class TestTab extends BaseApi {
constructor (fileManager, filePanel, compileTab, appStore) {
super(profile)
this.compileTab = compileTab
this._view = { el: null }
this.compileTab = compileTab
this.fileManager = fileManager
this.filePanel = filePanel
this.appStore = appStore
this.testTabLogic = new TestTabLogic(fileManager)
this.data = {}
this.appStore.event.on('activate', (name) => {
if (name === 'solidity') this.updateRunAction(fileManager.currentFile())
})
this.appStore.event.on('deactivate', (name) => {
if (name === 'solidity') this.updateRunAction(fileManager.currentFile())
})
}
activate () {
this.listenToEvents()
}
deactivate () {
}
listenToEvents () {
this.filePanel.event.register('newTestFileCreated', file => {
var testList = this.view.querySelector("[class^='testList']")
var test = yo`<label class="singleTestLabel"><input class="singleTest" onchange=${(e) => this.toggleCheckbox(e.target.checked, file)} type="checkbox" checked="true">${file}</label>`
testList.appendChild(test)
this.data.allTests.push(file)
this.data.selectedTests.push(file)
})
this.fileManager.events.on('noFileSelected', () => {
this.updateGenerateFileAction()
this.updateRunAction()
this.updateTestFileList()
})
this.fileManager.events.on('currentFileChanged', (file, provider) => {
this.updateGenerateFileAction(file)
this.updateRunAction(file)
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 || !this.testsSummary) return
})
})
}
listTests () {
return this.data.allTests.map(test => yo`<label class="singleTestLabel"><input class="singleTest" onchange =${(e) => this.toggleCheckbox(e.target.checked, test)} type="checkbox" checked="true">${test} </label>`)
}
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
let checkAll = this._view.el.querySelector('[id="checkAllTests"]')
if (eChecked) {
checkAll.checked = true
} else if (!selectedTests.length) {
checkAll.checked = false
}
}
checkAll (event) {
let 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)
}
}
testCallback (result) {
this.testsOutput.hidden = false
if (result.type === 'contract') {
this.testsOutput.appendChild(yo`<div class=${css.outputTitle}>${result.filename} (${result.value})</div>`)
} else if (result.type === 'testPass') {
this.testsOutput.appendChild(yo`<div class="${css.testPass} ${css.testLog} bg-success">✓ (${result.value})</div>`)
} else if (result.type === 'testFailure') {
this.testsOutput.appendChild(yo`<div class="${css.testFailure} ${css.testLog} bg-danger">✘ (${result.value})</div>`)
}
}
resultsCallback (_err, result, cb) {
// total stats for the test
// result.passingNum
// result.failureNum
// result.timePassed
cb()
}
updateFinalResult (_err, result, filename) {
this.testsSummary.hidden = false
if (_err) {
this.testsSummary.appendChild(yo`<div class="${css.testFailureSummary} text-danger" >${_err.message}</div>`)
return
}
this.testsSummary.appendChild(yo`<div class=${css.summaryTitle}> ${filename} </div>`)
if (result.totalPassing > 0) {
this.testsSummary.appendChild(yo`<div class="text-success" >${result.totalPassing} passing (${result.totalTime}s)</div>`)
this.testsSummary.appendChild(yo`<br>`)
}
if (result.totalFailing > 0) {
this.testsSummary.appendChild(yo`<div class="text-danger" >${result.totalFailing} failing</div>`)
this.testsSummary.appendChild(yo`<br>`)
}
result.errors.forEach((error, index) => {
this.testsSummary.appendChild(yo`<div class="text-danger" >${error.context} - ${error.value} </div>`)
this.testsSummary.appendChild(yo`<div class="${css.testFailureSummary} text-danger" >${error.message}</div>`)
this.testsSummary.appendChild(yo`<br>`)
})
}
runTest (testFilePath, callback) {
this.loading.hidden = false
this.fileManager.fileProviderOf(testFilePath).get(testFilePath, (error, content) => {
if (error) return
var runningTest = {}
runningTest[testFilePath] = { content }
remixTests.runTestSources(runningTest, (result) => { this.testCallback(result) }, (_err, result, cb) => { this.resultsCallback(_err, result, cb) }, (error, result) => {
this.updateFinalResult(error, result, testFilePath)
this.loading.hidden = true
callback(error)
}, (url, cb) => {
return this.compileTab.compileTabLogic.importFileCb(url, cb)
})
})
}
runTests () {
this.testsOutput.innerHTML = ''
this.testsSummary.innerHTML = ''
var tests = this.data.selectedTests
if (!tests) return
this.loading.hidden = tests.length === 0
async.eachOfSeries(tests, (value, key, callback) => { this.runTest(value, callback) })
}
updateGenerateFileAction (currentFile) {
let el = yo`<button class="${css.generateTestFile} btn btn-primary" onclick="${this.testTabLogic.generateTestFile.bind(this.testTabLogic)}">Generate test file</button>`
if (!currentFile) {
el.setAttribute('disabled', 'disabled')
el.setAttribute('title', 'No file selected')
}
if (!this.generateFileActionElement) {
this.generateFileActionElement = el
} else {
yo.update(this.generateFileActionElement, el)
}
return this.generateFileActionElement
}
updateRunAction (currentFile) {
let el = yo`<button class="${css.runButton} btn btn-primary" onclick="${this.runTests.bind(this)}">Run Tests</button>`
const isSolidityActive = this.appStore.isActive('solidity')
if (!currentFile || !isSolidityActive) {
el.setAttribute('disabled', 'disabled')
if (!currentFile) el.setAttribute('title', 'No file selected')
if (!isSolidityActive) el.setAttribute('title', 'The solidity module should be activated')
}
if (!this.runActionElement) {
this.runActionElement = el
} else {
yo.update(this.runActionElement, el)
}
return this.runActionElement
}
updateTestFileList (tests) {
const testsMessage = (tests && tests.length ? this.listTests() : 'No test file available')
let el = yo`<div class=${css.testList}>${testsMessage}</div>`
if (!this.testFilesListElement) {
this.testFilesListElement = el
} else {
yo.update(this.testFilesListElement, el)
}
return this.testFilesListElement
}
render () {
this.testsOutput = yo`<div class="${css.container} m-3 border border-primary border-right-0 border-left-0 border-bottom-0" hidden='true' id="tests"></div>`
this.testsSummary = yo`<div class="${css.container} border border-primary border-right-0 border-left-0 border-bottom-0" hidden='true' id="tests"></div>`
this.loading = yo`<span class='text-info ml-1'>Running tests...</span>`
this.loading.hidden = true
var el = yo`
<div class="${css.testTabView} card" id="testView">
<div class="${css.infoBox}">
Test your smart contract by creating a foo_test.sol file (open ballot_test.sol to see the example).
<br/>
You will find more informations in the <a href="https://remix.readthedocs.io/en/latest/unittesting_tab.html">documentation</a>
Then use the stand alone NPM module remix-tests to run unit tests in your Continuous Integration
<a href="https://www.npmjs.com/package/remix-tests">https://www.npmjs.com/package/remix-tests</a>.
<br/>
For more details, see
How to test smart contracts guide in our documentation.
<br/>
${this.updateGenerateFileAction()}
</div>
<div class="${css.tests}">
<div class="${css.buttons}">
${this.updateRunAction()}
<label class="${css.label} mx-4 m-2" for="checkAllTests">
<input id="checkAllTests"
type="checkbox"
onclick="${(event) => { this.checkAll(event) }}"
checked="true"
>
Check/Uncheck all
</label>
</div>
${this.updateTestFileList()}
<hr>
<div class="${css.buttons}" ><h6>Results:${this.loading}</h6></div>
${this.testsOutput}
${this.testsSummary}
</div>
</div>
`
if (!this._view.el) this._view.el = el
return el
}
}