Merge branch 'master' of github.com:ethereum/remix-project

pull/1861/head
Joseph Izang 3 years ago
commit 59be4bfc85
  1. 35
      .eslintrc.json
  2. 14
      apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts
  3. 763
      apps/remix-ide/src/app/tabs/test-tab.js
  4. 135
      apps/remix-ide/src/app/tabs/testTab/testTab.js
  5. 4
      libs/remix-ui/solidity-unit-testing/.babelrc
  6. 21
      libs/remix-ui/solidity-unit-testing/.eslintrc.json
  7. 7
      libs/remix-ui/solidity-unit-testing/README.md
  8. 2
      libs/remix-ui/solidity-unit-testing/src/index.ts
  9. 45
      libs/remix-ui/solidity-unit-testing/src/lib/css/style.css
  10. 141
      libs/remix-ui/solidity-unit-testing/src/lib/logic/testTabLogic.ts
  11. 728
      libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx
  12. 20
      libs/remix-ui/solidity-unit-testing/tsconfig.json
  13. 13
      libs/remix-ui/solidity-unit-testing/tsconfig.lib.json
  14. 3
      nx.json
  15. 3
      package.json
  16. 5
      tsconfig.base.json
  17. 19
      workspace.json

@ -0,0 +1,35 @@
{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nrwl/nx"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
}
]
}
]
}
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nrwl/nx/typescript"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nrwl/nx/javascript"],
"rules": {}
}
]
}

@ -64,9 +64,11 @@ module.exports = {
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '✓ Initial value should be100', 120000)
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '✓ Value is set200', 120000)
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', '✘ Should fail for wrong value200', 120000)
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'Passing: 2', 120000)
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'Failing: 1', 120000)
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'FAIL MyTest (tests/simple_storage_test.sol)', 120000)
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'Passed: 2', 120000)
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'Failed: 1', 120000)
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'FAILMyTest (tests/simple_storage_test.sol)', 120000)
// '.failed_tests_simple_storage_test_solMyTest' is the class for 'FAIL' label
.verify.elementPresent('.failed_tests_simple_storage_test_solMyTest')
},
'Should run advance unit test using natspec and experimental ABIEncoderV2 `ks2b_test.sol` #group2': function (browser: NightwatchBrowser) {
@ -94,10 +96,9 @@ module.exports = {
.waitForElementPresent('*[data-id="testTabRunTestsTabRunAction"]')
.clickElementAtPosition('.singleTestLabel', 0)
.scrollAndClick('*[data-id="testTabRunTestsTabRunAction"]')
.pause(2000)
.click('*[data-id="testTabRunTestsTabStopAction"]')
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'tests/ks2b_test.sol', 200000)
.notContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'tests/4_Ballot_test.sol')
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'tests/4_Ballot_test.sol', 200000)
.notContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'tests/ks2b_test.sol')
.notContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'tests/simple_storage_test.sol')
.waitForElementContainsText('*[data-id="testTabTestsExecutionStopped"]', 'The test execution has been stopped', 60000)
},
@ -151,6 +152,7 @@ module.exports = {
.waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]')
.addFile('myTests/simple_storage_test.sol', sources[0]['tests/simple_storage_test.sol'])
.clickLaunchIcon('solidityUnitTesting')
.clearValue('*[data-id="uiPathInput"]')
.setValue('*[data-id="uiPathInput"]', 'myTests')
.click('*[data-id="testTabGenerateTestFolder"]')
.clickElementAtPosition('.singleTest', 0, { forceSelectIfUnselected: true })

@ -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 helper 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}</label>
</div>
`
}
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 + '&emsp;' + 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>
</div>`
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})</span>
</div>
`
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}</span>
${debugBtn}
</div>
</div>
`)
} 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}</span>
${debugBtn}
</div>
<span class="text-dark">Error Message:</span>
<span class="pb-2 text-break">"${result.errMsg}"</span>
</div>
`)
} 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}</span>
${debugBtn}
</div>
<span class="text-dark">Error Message:</span>
<span class="pb-2 text-break">"${result.errMsg}"</span>
<span class="text-dark">Assertion:</span>
<div class="d-flex flex-wrap">
<span>Expected value should be</span>
<div class="mx-1 font-weight-bold">${method}</div>
<div>${preposition} ${expected}</div>
</div>
<span class="text-dark">Received value:</span>
<span>${result.returned}</span>
<span class="text-dark text-sm pb-2">Skipping the remaining tests of the function.</span>
</div>
`)
}
} 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
</div>
`
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})</span>
</div>
`)
} 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
</div>
`
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})</span>
</div>
`)
}
}
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}</span>
<span class="text-success">Passing: ${result.totalPassing}</span>
<span class="text-danger">Failing: ${result.totalFailing}</span>
<span>Total time: ${totalTime}s</span>
</div>
`)
} 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}</span>
<span class="text-success">Passing: ${result.totalPassing}</span>
<span>Total time: ${totalTime}s</span>
</div>
`)
} 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}</span>
<span class="text-danger">Failing: ${result.totalFailing}</span>
<span>Total time: ${totalTime}s</span>
</div>
`)
}
// 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>
</div>
`)
}
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
</button>
`
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"></span>
<label class="${css.labelOnBtn} btn btn-primary p-1 ml-2 m-0">Run</label>
</button>
`
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"></span>
<label class="${css.labelOnBtn} btn btn-secondary p-1 ml-2 m-0" id="runTestsTabStopActionLabel">Stop</label>
</button>
`
}
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 </label>
</div>
`
}
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...</label>
</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
</button>
`
this.renderComponent('tests')
return this.element
}
const availablePaths = yo`
<div>
<div class="d-flex p-2">
${this.inputPath}
${this.createTestFolder}
${this.uiPathList}
</div>
</div>
`
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:</label>
${availablePaths}
</div>
<div class="${css.tests}">
<div class="d-flex p-2">
${this.updateGenerateFileAction()}
${this.infoButton()}
</div>
<div class="d-flex p-2">
${this.updateRunAction()}
${this.updateStopAction()}
</div>
${this.selectAll()}
${this.updateTestFileList()}
<div class="align-items-start flex-column mt-2 mx-3 mb-0">
${this.resultStatistics}
${this.testsExecutionStopped}
${this.testsExecutionStoppedError}
</div>
${this.testsOutput}
</div>
</div>
`
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)
}
}

@ -1,135 +0,0 @@
const helper = require('../../../lib/helper.js')
const modalDialogCustom = require('../../ui/modal-dialog-custom')
const remixPath = require('path')
class TestTabLogic {
constructor (fileManager) {
this.fileManager = fileManager
this.currentPath = '/tests'
}
setCurrentPath (path) {
if (path.indexOf('/') === 0) return
this.currentPath = helper.removeMultipleSlashes(helper.removeTrailingSlashes(path))
}
generateTestFolder (path) {
// Todo move this check to File Manager after refactoring
// Checking to ignore the value which contains only whitespaces
if (!path || !(/\S/.test(path))) return
path = helper.removeMultipleSlashes(path)
const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0])
fileProvider.exists(path).then(res => {
if (!res) fileProvider.createDir(path)
})
}
async pathExists (path) {
// Checking to ignore the value which contains only whitespaces
if (!path || !(/\S/.test(path))) return
const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0])
const res = await fileProvider.exists(path, (e, res) => { return res })
return res
}
generateTestFile () {
let fileName = this.fileManager.currentFile()
const hasCurrent = !!fileName && this.fileManager.currentFile().split('.').pop().toLowerCase() === 'sol'
if (!hasCurrent) fileName = this.currentPath + '/newFile.sol'
const fileProvider = this.fileManager.fileProviderOf(this.currentPath)
if (!fileProvider) return
const splittedFileName = fileName.split('/')
const fileNameToImport = (!hasCurrent) ? fileName : this.currentPath + '/' + splittedFileName[splittedFileName.length - 1]
helper.createNonClashingNameWithPrefix(fileNameToImport, fileProvider, '_test', (error, newFile) => {
if (error) return modalDialogCustom.alert('Failed to create file. ' + newFile + ' ' + error)
if (!fileProvider.set(newFile, this.generateTestContractSample(hasCurrent, fileName))) return modalDialogCustom.alert('Failed to create test file ' + newFile)
this.fileManager.open(newFile)
this.fileManager.syncEditor(newFile)
})
}
dirList (path) {
return this.fileManager.dirList(path)
}
isRemixDActive () {
return this.fileManager.isRemixDActive()
}
async getTests (cb) {
if (!this.currentPath) return cb(null, [])
const provider = this.fileManager.fileProviderOf(this.currentPath)
if (!provider) return cb(null, [])
const tests = []
let files = []
try {
if (await this.fileManager.exists(this.currentPath)) files = await this.fileManager.readdir(this.currentPath)
} catch (e) {
cb(e.message)
}
for (var file in files) {
const filepath = provider && provider.type ? provider.type + '/' + file : file
if (/.(_test.sol)$/.exec(file)) tests.push(filepath)
}
cb(null, tests, this.currentPath)
}
// @todo(#2758): If currently selected file is compiled and compilation result is available,
// 'contractName' should be <compiledContractName> + '_testSuite'
generateTestContractSample (hasCurrent, fileToImport, contractName = 'testSuite') {
let relative = remixPath.relative(this.currentPath, remixPath.dirname(fileToImport))
if (relative === '') relative = '.'
const comment = hasCurrent ? `import "${relative}/${remixPath.basename(fileToImport)}";` : '// <import file to test>'
return `// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
// This import is automatically injected by Remix
import "remix_tests.sol";
// This import is required to use custom transaction context
// Although it may fail compilation in 'Solidity Compiler' plugin
// But it will work fine in 'Solidity Unit Testing' plugin
import "remix_accounts.sol";
${comment}
// File name has to end with '_test.sol', this file can contain more than one testSuite contracts
contract ${contractName} {
/// 'beforeAll' runs before all other tests
/// More special functions are: 'beforeEach', 'beforeAll', 'afterEach' & 'afterAll'
function beforeAll() public {
// <instantiate contract>
Assert.equal(uint(1), uint(1), "1 should be equal to 1");
}
function checkSuccess() public {
// Use 'Assert' methods: https://remix-ide.readthedocs.io/en/latest/assert_library.html
Assert.ok(2 == 2, 'should be true');
Assert.greaterThan(uint(2), uint(1), "2 should be greater than to 1");
Assert.lesserThan(uint(2), uint(3), "2 should be lesser than to 3");
}
function checkSuccess2() public pure returns (bool) {
// Use the return value (true or false) to test the contract
return true;
}
function checkFailure() public {
Assert.notEqual(uint(1), uint(1), "1 should not be equal to 1");
}
/// Custom Transaction Context: https://remix-ide.readthedocs.io/en/latest/unittesting.html#customization
/// #sender: account-1
/// #value: 100
function checkSenderAndValue() public payable {
// account index varies 0-9, value is in wei
Assert.equal(msg.sender, TestsAccounts.getAccount(1), "Invalid sender");
Assert.equal(msg.value, 100, "Invalid value");
}
}
`
}
}
module.exports = TestTabLogic

@ -0,0 +1,4 @@
{
"presets": ["@nrwl/react/babel"],
"plugins": []
}

@ -0,0 +1,21 @@
{
"extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
},
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

@ -0,0 +1,7 @@
# remix-ui-solidity-unit-testing
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test remix-ui-solidity-unit-testing` to execute the unit tests via [Jest](https://jestjs.io).

@ -0,0 +1,2 @@
export * from './lib/solidity-unit-testing'
export * from './lib/logic/testTabLogic'

@ -0,0 +1,45 @@
.infoBox {
margin: 5%;
}
.testList {
line-height: 2em;
display: flex;
flex-direction: column;
margin: 5%;
max-height: 300px;
overflow-y: auto;
}
.container {
margin: 2%;
padding-bottom: 5%;
max-height: 300px;
overflow-y: auto;
}
.summaryTitle {
font-weight: bold;
}
.testPass {
}
.testLog {
margin-bottom: 1%;
border-radius: 4px;
padding: 1% 1% 1% 5%;
}
.title {
font-size: 1.1em;
font-weight: bold;
margin-bottom: 1em;
}
.label {
display: flex;
align-items: center;
white-space: nowrap;
}
.labelOnBtn {
border: hidden;
}
.inputFolder {
width: 80%;
}

@ -0,0 +1,141 @@
import remixPath from 'path'
export class TestTabLogic {
fileManager
currentPath
helper
constructor (fileManager: any, helper: any) {
this.fileManager = fileManager
this.helper = helper
this.currentPath = '/tests'
}
setCurrentPath (path: string) {
if (path === '/') {
this.currentPath = '/'
return
}
if (path.indexOf('/') === 0) return
this.currentPath = this.helper.removeMultipleSlashes(this.helper.removeTrailingSlashes(path))
}
generateTestFolder (path:string) {
// Todo move this check to File Manager after refactoring
// Checking to ignore the value which contains only whitespaces
if (!path || !(/\S/.test(path))) return
path = this.helper.removeMultipleSlashes(path)
const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0])
fileProvider.exists(path).then((res: any) => {
if (!res) fileProvider.createDir(path)
})
}
async pathExists (path: string) {
// Checking to ignore the value which contains only whitespaces
if (!path || !(/\S/.test(path))) return
const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0])
const res = await fileProvider.exists(path, (e: any, res: any) => { return res })
return res
}
generateTestFile (errorCb: any) {
let fileName = this.fileManager.currentFile()
const hasCurrent = !!fileName && this.fileManager.currentFile().split('.').pop().toLowerCase() === 'sol'
if (!hasCurrent) fileName = this.currentPath + '/newFile.sol'
const fileProvider = this.fileManager.fileProviderOf(this.currentPath)
if (!fileProvider) return
const splittedFileName = fileName.split('/')
const fileNameToImport = (!hasCurrent) ? fileName : this.currentPath + '/' + splittedFileName[splittedFileName.length - 1]
this.helper.createNonClashingNameWithPrefix(fileNameToImport, fileProvider, '_test', (error: any, newFile: any) => {
if (error) return errorCb('Failed to create file. ' + newFile + ' ' + error)
const isFileCreated = fileProvider.set(newFile, this.generateTestContractSample(hasCurrent, fileName))
if (!isFileCreated) return errorCb('Failed to create test file ' + newFile)
this.fileManager.open(newFile)
this.fileManager.syncEditor(newFile)
})
}
dirList (path:string) {
return this.fileManager.dirList(path)
}
isRemixDActive () {
return this.fileManager.isRemixDActive()
}
async getTests () {
if (!this.currentPath) return []
const provider = this.fileManager.fileProviderOf(this.currentPath)
if (!provider) return []
const tests = []
let files = []
try {
if (await this.fileManager.exists(this.currentPath)) files = await this.fileManager.readdir(this.currentPath)
} catch (e: any) {
throw e.message
}
for (const file in files) {
const filepath = provider && provider.type ? provider.type + '/' + file : file
if (/.(_test.sol)$/.exec(file)) tests.push(filepath)
}
return tests
}
// @todo(#2758): If currently selected file is compiled and compilation result is available,
// 'contractName' should be <compiledContractName> + '_testSuite'
generateTestContractSample (hasCurrent: any, fileToImport: any, contractName = 'testSuite') {
let relative = remixPath.relative(this.currentPath, remixPath.dirname(fileToImport))
if (relative === '') relative = '.'
const comment = hasCurrent ? `import "${relative}/${remixPath.basename(fileToImport)}";` : '// <import file to test>'
return `// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
// This import is automatically injected by Remix
import "remix_tests.sol";
// This import is required to use custom transaction context
// Although it may fail compilation in 'Solidity Compiler' plugin
// But it will work fine in 'Solidity Unit Testing' plugin
import "remix_accounts.sol";
${comment}
// File name has to end with '_test.sol', this file can contain more than one testSuite contracts
contract ${contractName} {
/// 'beforeAll' runs before all other tests
/// More special functions are: 'beforeEach', 'beforeAll', 'afterEach' & 'afterAll'
function beforeAll() public {
// <instantiate contract>
Assert.equal(uint(1), uint(1), "1 should be equal to 1");
}
function checkSuccess() public {
// Use 'Assert' methods: https://remix-ide.readthedocs.io/en/latest/assert_library.html
Assert.ok(2 == 2, 'should be true');
Assert.greaterThan(uint(2), uint(1), "2 should be greater than to 1");
Assert.lesserThan(uint(2), uint(3), "2 should be lesser than to 3");
}
function checkSuccess2() public pure returns (bool) {
// Use the return value (true or false) to test the contract
return true;
}
function checkFailure() public {
Assert.notEqual(uint(1), uint(1), "1 should not be equal to 1");
}
/// Custom Transaction Context: https://remix-ide.readthedocs.io/en/latest/unittesting.html#customization
/// #sender: account-1
/// #value: 100
function checkSenderAndValue() public payable {
// account index varies 0-9, value is in wei
Assert.equal(msg.sender, TestsAccounts.getAccount(1), "Invalid sender");
Assert.equal(msg.value, 100, "Invalid value");
}
}
`
}
}

@ -0,0 +1,728 @@
import React, { useState, useRef, useEffect } from 'react' // eslint-disable-line
import { eachOfSeries } from 'async' // eslint-disable-line
import { canUseWorker, urlFromVersion } from '@remix-project/remix-solidity'
import { Renderer } from '@remix-ui/renderer' // eslint-disable-line
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
import { format } from 'util'
import './css/style.css'
const _paq = (window as any)._paq = (window as any)._paq || [] // eslint-disable-line
/* eslint-disable-next-line */
export interface SolidityUnitTestingProps { }
interface TestObject {
fileName: string
checked: boolean
}
export const SolidityUnitTesting = (props: Record<string, any>) => {
const { helper, testTab, initialPath } = props
const { testTabLogic } = testTab
const [toasterMsg, setToasterMsg] = useState('')
const [disableCreateButton, setDisableCreateButton] = useState(true)
const [disableGenerateButton, setDisableGenerateButton] = useState(false)
const [disableStopButton, setDisableStopButton] = useState(true)
const [disableRunButton, setDisableRunButton] = useState(false)
const [runButtonTitle, setRunButtonTitle] = useState('Run tests')
const [stopButtonLabel, setStopButtonLabel] = useState('Stop')
const [checkSelectAll, setCheckSelectAll] = useState(true)
const [testsOutput, setTestsOutput] = useState<Element[]>([])
const [testsExecutionStoppedHidden, setTestsExecutionStoppedHidden] = useState(true)
const [progressBarHidden, setProgressBarHidden] = useState(true)
const [testsExecutionStoppedErrorHidden, setTestsExecutionStoppedErrorHidden] = useState(true)
let [testFiles, setTestFiles] = useState<TestObject[]>([]) // eslint-disable-line
const [pathOptions, setPathOptions] = useState([''])
const [inputPathValue, setInputPathValue] = useState('tests')
let [readyTestsNumber, setReadyTestsNumber] = useState(0) // eslint-disable-line
let [runningTestsNumber, setRunningTestsNumber] = useState(0) // eslint-disable-line
const hasBeenStopped = useRef(false)
const isDebugging = useRef(false)
const allTests: any = useRef([])
const selectedTests: any = useRef([])
const currentErrors: any = useRef([])
const defaultPath = 'tests'
let areTestsRunning = false
let runningTestFileName: any
const filesContent: any = {}
const testsResultByFilename: Record<string, any> = {}
const trimTestDirInput = (input: string) => {
if (input.includes('/')) return input.split('/').map(e => e.trim()).join('/')
else return input.trim()
}
const clearResults = () => {
setProgressBarHidden(true)
testTab.call('editor', 'clearAnnotations')
setTestsOutput([])
setTestsExecutionStoppedHidden(true)
setTestsExecutionStoppedErrorHidden(true)
}
const updateForNewCurrent = async (file = null) => {
// 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 (currentErrors.current) {
if (Array.isArray(currentErrors.current) && currentErrors.current.length > 0) {
const errFiles = currentErrors.current.map((err:any) => { if (err.sourceLocation && err.sourceLocation.file) return err.sourceLocation.file }) // eslint-disable-line
if (errFiles.includes(file)) return
} else if (currentErrors.current.sourceLocation && currentErrors.current.sourceLocation.file && currentErrors.current.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 (isDebugging.current && testTab.allFilesInvolved.includes(file)) return
allTests.current = []
updateTestFileList()
clearResults()
try {
const tests = await testTabLogic.getTests()
allTests.current = tests
selectedTests.current = [...allTests.current]
updateTestFileList()
if (!areTestsRunning) await updateRunAction(file)
} catch (e: any) {
console.log(e)
setToasterMsg(e)
}
}
/**
* Changes the current path of Unit Testing Plugin
* @param path - the path from where UT plugin takes _test.sol files to run
*/
const setCurrentPath = async (path: string) => {
testTabLogic.setCurrentPath(path)
setInputPathValue(path)
updateDirList(path)
await updateForNewCurrent()
}
useEffect(() => {
if (initialPath) setCurrentPath(initialPath)
}, [initialPath]) // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
testTab.on('filePanel', 'newTestFileCreated', async (file: string) => {
try {
const tests = await testTabLogic.getTests()
allTests.current = tests
selectedTests.current = [...allTests.current]
updateTestFileList()
} catch (e) {
console.log(e)
allTests.current.push(file)
selectedTests.current.push(file)
}
})
testTab.on('filePanel', 'setWorkspace', async () => {
await setCurrentPath(defaultPath)
})
testTab.fileManager.events.on('noFileSelected', () => { }) // eslint-disable-line
testTab.fileManager.events.on('currentFileChanged', async (file: any, provider: any) => await updateForNewCurrent(file))
}, []) // eslint-disable-line
const updateDirList = (path: string) => {
testTabLogic.dirList(path).then((options: any) => {
setPathOptions(options)
})
}
const handleTestDirInput = async (e: any) => {
let testDirInput = trimTestDirInput(e.target.value)
testDirInput = helper.removeMultipleSlashes(testDirInput)
if (testDirInput !== '/') testDirInput = helper.removeTrailingSlashes(testDirInput)
setInputPathValue(testDirInput)
if (e.key === 'Enter') {
if (await testTabLogic.pathExists(testDirInput)) {
testTabLogic.setCurrentPath(testDirInput)
await updateForNewCurrent()
return
}
}
if (testDirInput) {
if (testDirInput.endsWith('/') && testDirInput !== '/') {
testDirInput = helper.removeTrailingSlashes(testDirInput)
if (testTabLogic.currentPath === testDirInput.substr(0, testDirInput.length - 1)) {
setDisableCreateButton(true)
setDisableGenerateButton(true)
}
updateDirList(testDirInput)
} else {
// If there is no matching folder in the workspace with entered text, enable Create button
if (await testTabLogic.pathExists(testDirInput)) {
setDisableCreateButton(true)
setDisableGenerateButton(false)
} else {
// Enable Create button
setDisableCreateButton(false)
// Disable Generate button because dir does not exist
setDisableGenerateButton(true)
}
await setCurrentPath(testDirInput)
}
} else {
await setCurrentPath('/')
setDisableCreateButton(true)
setDisableGenerateButton(false)
}
}
const handleEnter = async (e: any) => {
let inputPath = e.target.value
inputPath = helper.removeMultipleSlashes(trimTestDirInput(inputPath))
setInputPathValue(inputPath)
if (disableCreateButton) {
if (await testTabLogic.pathExists(inputPath)) {
await setCurrentPath(inputPath)
}
}
}
const handleCreateFolder = async () => {
let inputPath = trimTestDirInput(inputPathValue)
let path = helper.removeMultipleSlashes(inputPath)
if (path !== '/') path = helper.removeTrailingSlashes(path)
if (inputPath === '') inputPath = defaultPath
setInputPathValue(path)
testTabLogic.generateTestFolder(inputPath)
setDisableCreateButton(true)
setDisableGenerateButton(false)
testTabLogic.setCurrentPath(inputPath)
await updateRunAction()
await updateForNewCurrent()
pathOptions.push(inputPath)
setPathOptions(pathOptions)
}
const cleanFileName = (fileName: any, testSuite: any) => {
return fileName ? fileName.replace(/\//g, '_').replace(/\./g, '_') + testSuite : fileName
}
const startDebug = async (txHash: any, web3: any) => {
isDebugging.current = true
if (!await testTab.appManager.isActive('debugger')) await testTab.appManager.activatePlugin('debugger')
testTab.call('menuicons', 'select', 'debugger')
testTab.call('debugger', 'debug', txHash, web3)
}
const printHHLogs = (logsArr: any, testName: any) => {
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 + '&emsp;' + formattedLog + '\n'
}
_paq.push(['trackEvent', 'solidityUnitTesting', 'hardhat', 'console.log'])
testTab.call('terminal', 'log', { type: 'info', value: finalLogs })
}
const discardHighlight = async () => {
await testTab.call('editor', 'discardHighlight')
}
const highlightLocation = async (location: string, fileName: string) => {
if (location) {
const split = location.split(':')
const file = split[2]
const parsedLocation = {
start: parseInt(split[0]),
length: parseInt(split[1])
}
const locationToHighlight = testTab.offsetToLineColumnConverter.offsetToLineColumnWithContent(
parsedLocation,
parseInt(file),
filesContent[fileName].content
)
await testTab.call('editor', 'discardHighlight')
await testTab.call('editor', 'highlight', locationToHighlight, fileName, '', { focus: true })
}
}
const renderContract = (filename: any, contract: any, index: number, withoutLabel = false) => {
if (withoutLabel) {
const contractCard: any = (
<div id={runningTestFileName} data-id="testTabSolidityUnitTestsOutputheader" className="pt-1">
<span className="font-weight-bold">{contract ? contract : ''} ({filename})</span>
</div>
)
setTestsOutput(prevCards => ([...prevCards, contractCard]))
return
}
let label
if (index > -1) {
const className = "alert-danger d-inline-block mb-1 mr-1 p-1 failed_" + runningTestFileName
label = (<div
className={className}
title="At least one contract test failed"
>
FAIL
</div>)
} else {
const className = "alert-success d-inline-block mb-1 mr-1 p-1 passed_" + runningTestFileName
label = (<div
className={className}
title="All contract tests passed"
>
PASS
</div>)
}
// show contract and file name with label
const ContractCard: any = (
<div id={runningTestFileName} data-id="testTabSolidityUnitTestsOutputheader" className="pt-1">
{label}<span className="font-weight-bold">{contract} ({filename})</span>
</div>
)
setTestsOutput(prevCards => {
const index = prevCards.findIndex((card: any) => card.props.id === runningTestFileName)
prevCards[index] = ContractCard
return prevCards
})
}
const renderTests = (tests: any, contract: any, filename: any) => {
const index = tests.findIndex((test: any) => test.type === 'testFailure')
// show filename and contract
renderContract(filename, contract, index)
// show tests
for (const test of tests) {
if (!test.rendered) {
let debugBtn
if (test.debugTxHash) {
const { web3, debugTxHash } = test
debugBtn = (
<div id={test.value.replaceAll(' ', '_')} className="btn border btn btn-sm ml-1" style={{ cursor: 'pointer' }} title="Start debugging" onClick={() => startDebug(debugTxHash, web3)}>
<i className="fas fa-bug"></i>
</div>
)
}
if (test.type === 'testPass') {
if (test.hhLogs && test.hhLogs.length) printHHLogs(test.hhLogs, test.value)
const testPassCard: any = (
<div
id={runningTestFileName}
data-id="testTabSolidityUnitTestsOutputheader"
className="testPass testLog bg-light mb-2 px-2 text-success border-0"
onClick={() => discardHighlight()}
>
<div className="d-flex my-1 align-items-start justify-content-between">
<span > {test.value}</span>
{debugBtn}
</div>
</div>
)
setTestsOutput(prevCards => ([...prevCards, testPassCard]))
test.rendered = true
} else if (test.type === 'testFailure') {
if (test.hhLogs && test.hhLogs.length) printHHLogs(test.hhLogs, test.value)
if (!test.assertMethod) {
const testFailCard1: any = (<div
className="bg-light mb-2 px-2 testLog d-flex flex-column text-danger border-0"
id={"UTContext" + test.context}
onClick={() => highlightLocation(test.location, test.filename)}
>
<div className="d-flex my-1 align-items-start justify-content-between">
<span> {test.value}</span>
{debugBtn}
</div>
<span className="text-dark">Error Message:</span>
<span className="pb-2 text-break">"{test.errMsg}"</span>
</div>)
setTestsOutput(prevCards => ([...prevCards, testFailCard1]))
} else {
const preposition = test.assertMethod === 'equal' || test.assertMethod === 'notEqual' ? 'to' : ''
const method = test.assertMethod === 'ok' ? '' : test.assertMethod
const expected = test.assertMethod === 'ok' ? '\'true\'' : test.expected
const testFailCard2: any = (<div
className="bg-light mb-2 px-2 testLog d-flex flex-column text-danger border-0"
id={"UTContext" + test.context}
onClick={() => highlightLocation(test.location, test.filename)}
>
<div className="d-flex my-1 align-items-start justify-content-between">
<span> {test.value}</span>
{debugBtn}
</div>
<span className="text-dark">Error Message:</span>
<span className="pb-2 text-break">"{test.errMsg}"</span>
<span className="text-dark">Assertion:</span>
<div className="d-flex flex-wrap">
<span>Expected value should be</span>
<div className="mx-1 font-weight-bold">{method}</div>
<div>{preposition} {expected}</div>
</div>
<span className="text-dark">Received value:</span>
<span>{test.returned}</span>
<span className="text-dark text-sm pb-2">Skipping the remaining tests of the function.</span>
</div>)
setTestsOutput(prevCards => ([...prevCards, testFailCard2]))
}
test.rendered = true
} else if (test.type === 'logOnly') {
if (test.hhLogs && test.hhLogs.length) printHHLogs(test.hhLogs, test.value)
test.rendered = true
}
}
}
}
const showTestsResult = () => {
const filenames = Object.keys(testsResultByFilename)
for (const filename of filenames) {
const fileTestsResult = testsResultByFilename[filename]
const contracts = Object.keys(fileTestsResult)
for (const contract of contracts) {
if (contract && contract !== 'summary' && contract !== 'errors') {
runningTestFileName = cleanFileName(filename, contract)
const tests = fileTestsResult[contract]
if (tests?.length) {
renderTests(tests, contract, filename)
} else {
// show only contract and file name
renderContract(filename, contract, -1, true)
}
} else if (contract === 'errors' && fileTestsResult['errors']) {
const errors = fileTestsResult['errors']
if (errors && errors.errors) {
errors.errors.forEach((err: any) => {
const errorCard: any = <Renderer message={err.formattedMessage || err.message} plugin={testTab} opt={{ type: err.severity, errorType: err.type }} />
setTestsOutput(prevCards => ([...prevCards, errorCard]))
})
} else if (errors && Array.isArray(errors) && (errors[0].message || errors[0].formattedMessage)) {
errors.forEach((err) => {
const errorCard: any = <Renderer message={err.formattedMessage || err.message} plugin={testTab} opt={{ type: err.severity, errorType: err.type }} />
setTestsOutput(prevCards => ([...prevCards, errorCard]))
})
} else if (errors && !errors.errors && !Array.isArray(errors)) {
// To track error like this: https://github.com/ethereum/remix/pull/1438
const errorCard: any = <Renderer message={errors.formattedMessage || errors.message} plugin={testTab} opt={{ type: 'error' }} />
setTestsOutput(prevCards => ([...prevCards, errorCard]))
}
}
}
// show summary
const testSummary = fileTestsResult['summary']
if (testSummary && testSummary.filename && !testSummary.rendered) {
const summaryCard: any = (<div className="d-flex alert-secondary mb-3 p-3 flex-column">
<span className="font-weight-bold">Result for {testSummary.filename}</span>
<span className="text-success">Passed: {testSummary.passed}</span>
<span className="text-danger">Failed: {testSummary.failed}</span>
<span>Time Taken: {testSummary.timeTaken}s</span>
</div>)
setTestsOutput(prevCards => ([...prevCards, summaryCard]))
fileTestsResult['summary']['rendered'] = true
}
}
}
const testCallback = (result: any) => {
if (result.filename) {
if (!testsResultByFilename[result.filename]) {
testsResultByFilename[result.filename] = {}
testsResultByFilename[result.filename]['summary'] = {}
}
if (result.type === 'contract') {
testsResultByFilename[result.filename][result.value] = {}
testsResultByFilename[result.filename][result.value] = []
} else {
// Set that this test is not rendered on UI
result.rendered = false
testsResultByFilename[result.filename][result.context].push(result)
}
showTestsResult()
}
}
const resultsCallback = (_err: any, result: any, cb: any) => {
// total stats for the test
// result.passingNum
// result.failureNum
// result.timePassed
cb()
}
const updateFinalResult = (_errors: any, result: any, filename: any) => {
++readyTestsNumber
setReadyTestsNumber(readyTestsNumber)
if (!result && (_errors && (_errors.errors || (Array.isArray(_errors) && (_errors[0].message || _errors[0].formattedMessage))))) {
// show only file name
renderContract(filename, null, -1, true)
currentErrors.current = _errors.errors
}
if (result) {
const totalTime = parseFloat(result.totalTime).toFixed(2)
const testsSummary = { filename, passed: result.totalPassing, failed: result.totalFailing, timeTaken: totalTime, rendered: false }
testsResultByFilename[filename]['summary'] = testsSummary
showTestsResult()
} else if (_errors) {
if (!testsResultByFilename[filename]) {
testsResultByFilename[filename] = {}
}
testsResultByFilename[filename]['errors'] = _errors
setTestsExecutionStoppedErrorHidden(false)
showTestsResult()
}
if (hasBeenStopped.current && (readyTestsNumber !== runningTestsNumber)) {
// if all tests has been through before stopping no need to print this.
setTestsExecutionStoppedHidden(false)
}
if (_errors || hasBeenStopped.current || readyTestsNumber === runningTestsNumber) {
// All tests are ready or the operation has been canceled or there was a compilation error in one of the test files.
setDisableStopButton(true)
setStopButtonLabel('Stop')
if (selectedTests.current?.length !== 0) {
setDisableRunButton(false)
}
areTestsRunning = false
}
}
const runTest = (testFilePath: any, callback: any) => {
isDebugging.current = false
if (hasBeenStopped.current) {
updateFinalResult(null, null, null)
return
}
testTab.fileManager.readFile(testFilePath).then((content: any) => {
const runningTests: any = {}
runningTests[testFilePath] = { content }
filesContent[testFilePath] = { content }
const { currentVersion, evmVersion, optimize, runs, isUrl } = testTab.compileTab.getCurrentCompilerConfig()
const currentCompilerUrl = isUrl ? currentVersion : urlFromVersion(currentVersion)
const compilerConfig = {
currentCompilerUrl,
evmVersion,
optimize,
usingWorker: canUseWorker(currentVersion),
runs
}
const deployCb = async (file: any, contractAddress: any) => {
const compilerData = await testTab.call('compilerArtefacts', 'getCompilerAbstract', file)
await testTab.call('compilerArtefacts', 'addResolvedContract', contractAddress, compilerData)
}
testTab.testRunner.runTestSources(
runningTests,
compilerConfig,
(result: any) => testCallback(result),
(_err: any, result: any, cb: any) => resultsCallback(_err, result, cb),
deployCb,
(error: any, result: any) => {
updateFinalResult(error, result, testFilePath)
callback(error)
}, (url: any, cb: any) => {
return testTab.contentImport.resolveAndSave(url).then((result: any) => cb(null, result)).catch((error: any) => cb(error.message))
}, { testFilePath }
)
}).catch((error: any) => {
console.log(error)
if (error) return // eslint-disable-line
})
}
const runTests = () => {
areTestsRunning = true
hasBeenStopped.current = false
readyTestsNumber = 0
setReadyTestsNumber(readyTestsNumber)
runningTestsNumber = selectedTests.current.length
setRunningTestsNumber(runningTestsNumber)
setDisableStopButton(false)
clearResults()
setProgressBarHidden(false)
const tests = selectedTests.current
if (!tests || !tests.length) return
else setProgressBarHidden(false)
_paq.push(['trackEvent', 'solidityUnitTesting', 'runTests'])
eachOfSeries(tests, (value: any, key: any, callback: any) => {
if (hasBeenStopped.current) return
runTest(value, callback)
})
}
const updateRunAction = async (currentFile: any = null) => {
const isSolidityActive = await testTab.appManager.isActive('solidity')
if (!isSolidityActive || !selectedTests.current?.length) {
// setDisableRunButton(true)
if (!currentFile || (currentFile && currentFile.split('.').pop().toLowerCase() !== 'sol')) {
setRunButtonTitle('No solidity file selected')
} else {
setRunButtonTitle('The "Solidity Plugin" should be activated')
}
}
}
const stopTests = () => {
hasBeenStopped.current = true
setStopButtonLabel('Stopping')
setDisableStopButton(true)
setDisableRunButton(true)
}
const getCurrentSelectedTests = () => {
const selectedTestsList: TestObject[] = testFiles.filter(testFileObj => testFileObj.checked)
return selectedTestsList.map(testFileObj => testFileObj.fileName)
}
const toggleCheckbox = (eChecked: any, index: any) => {
testFiles[index].checked = eChecked
setTestFiles(testFiles)
selectedTests.current = getCurrentSelectedTests()
if (eChecked) {
setCheckSelectAll(true)
setDisableRunButton(false)
if ((readyTestsNumber === runningTestsNumber || hasBeenStopped.current) && stopButtonLabel.trim() === 'Stop') {
setRunButtonTitle('Run tests')
}
} else if (!selectedTests.current.length) {
setCheckSelectAll(false)
setDisableRunButton(true)
setRunButtonTitle('No test file selected')
} else setCheckSelectAll(false)
}
const checkAll = (event: any) => {
testFiles.forEach((testFileObj) => testFileObj.checked = event.target.checked)
setTestFiles(testFiles)
setCheckSelectAll(event.target.checked)
if (event.target.checked) {
selectedTests.current = getCurrentSelectedTests()
setDisableRunButton(false)
} else {
selectedTests.current = []
setDisableRunButton(true)
}
}
const updateTestFileList = () => {
if (allTests.current?.length) {
testFiles = allTests.current.map((testFile: any) => { return { 'fileName': testFile, 'checked': true } })
setCheckSelectAll(true)
}
else
testFiles = []
setTestFiles(testFiles)
}
return (
<div className="px-2" id="testView">
<Toaster message={toasterMsg} />
<div className="infoBox">
<p className="text-lg"> Test your smart contract in Solidity.</p>
<p> Select directory to load and generate test files.</p>
<label>Test directory:</label>
<div>
<div className="d-flex p-2">
<datalist id="utPathList">{
pathOptions.map(function (path) {
return <option key={path}>{path}</option>
})
}
</datalist>
<input
list="utPathList"
className="inputFolder custom-select"
id="utPath"
data-id="uiPathInput"
name="utPath"
value={inputPathValue}
title="Press 'Enter' to change the path for test files."
style={{ backgroundImage: "var(--primary)" }}
onKeyUp={handleTestDirInput}
onChange={handleEnter}
onClick = {() => { if (inputPathValue === '/') setInputPathValue('')} }
/>
<button
className="btn border ml-2"
data-id="testTabGenerateTestFolder"
title="Create a test folder"
disabled={disableCreateButton}
onClick={handleCreateFolder}
>
Create
</button>
</div>
</div>
</div>
<div>
<div className="d-flex p-2">
<button
className="btn border w-50"
data-id="testTabGenerateTestFile"
title="Generate sample test file."
disabled={disableGenerateButton}
onClick={async () => {
testTabLogic.generateTestFile((err:any) => { if (err) setToasterMsg(err)})
await updateForNewCurrent()
}}
>
Generate
</button>
<a className="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 className="btn p-1 ml-2 m-0">How to use...</label>
</a>
</div>
<div className="d-flex p-2">
<button id="runTestsTabRunAction" title={runButtonTitle} data-id="testTabRunTestsTabRunAction" className="w-50 btn btn-primary" disabled={disableRunButton} onClick={runTests}>
<span className="fas fa-play ml-2"></span>
<label className="labelOnBtn btn btn-primary p-1 ml-2 m-0">Run</label>
</button>
<button id="runTestsTabStopAction" data-id="testTabRunTestsTabStopAction" className="w-50 pl-2 ml-2 btn btn-secondary" disabled={disableStopButton} title="Stop running tests" onClick={stopTests}>
<span className="fas fa-stop ml-2"></span>
<label className="labelOnBtn btn btn-secondary p-1 ml-2 m-0" id="runTestsTabStopActionLabel">{stopButtonLabel}</label>
</button>
</div>
<div className="d-flex align-items-center mx-3 pb-2 mt-2 border-bottom">
<input id="checkAllTests"
type="checkbox"
data-id="testTabCheckAllTests"
onClick={checkAll}
checked={checkSelectAll}
onChange={() => { }} // eslint-disable-line
/>
<label className="text-nowrap pl-2 mb-0" htmlFor="checkAllTests"> Select all </label>
</div>
<div className="testList py-2 mt-0 border-bottom">{testFiles?.length ? testFiles.map((testFileObj: any, index) => {
const elemId = `singleTest${testFileObj.fileName}`
return (
<div className="d-flex align-items-center py-1" key={index}>
<input className="singleTest" id={elemId} onChange={(e) => toggleCheckbox(e.target.checked, index)} type="checkbox" checked={testFileObj.checked} />
<label className="singleTestLabel text-nowrap pl-2 mb-0" htmlFor={elemId}>{testFileObj.fileName}</label>
</div>
)
})
: "No test file available"} </div>
<div className="align-items-start flex-column mt-2 mx-3 mb-0">
<span className='text-info h6' hidden={progressBarHidden}>Progress: {readyTestsNumber} finished (of {runningTestsNumber})</span>
<label className="text-warning h6" data-id="testTabTestsExecutionStopped" hidden={testsExecutionStoppedHidden}>The test execution has been stopped</label>
<label className="text-danger h6" data-id="testTabTestsExecutionStoppedError" hidden={testsExecutionStoppedErrorHidden}>The test execution has been stopped because of error(s) in your test file</label>
</div>
<div id="solidityUnittestsOutput" data-id="testTabSolidityUnitTestsOutput">{testsOutput}</div>
</div>
</div>
)
}
export default SolidityUnitTesting

@ -0,0 +1,20 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": ["**/*.spec.ts", "**/*.spec.tsx"],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

@ -148,6 +148,9 @@
"remix-ui-theme-module": {
"tags": []
},
"solidity-unit-testing": {
"tags": []
},
"remix-ui-editor-context-view": {
"tags": []
}

@ -266,8 +266,11 @@
"eslint-config-prettier": "^6.11.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "2.20.2",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "4.2.1",
"eslint-plugin-react": "7.23.1",
"eslint-plugin-react-hooks": "4.2.0",
"eslint-plugin-standard": "4.0.1",
"events": "^3.0.0",
"execr": "^1.0.1",

@ -71,7 +71,10 @@
"@remix-ui/app": ["libs/remix-ui/app/src/index.ts"],
"@remix-ui/vertical-icons-panel": ["libs/remix-ui/vertical-icons-panel/src/index.ts"],
"@remix-ui/theme-module": ["libs/remix-ui/theme-module/src/index.ts"],
"@remix-ui/editor-context-view": ["libs/remix-ui/editor-context-view/src/index.ts"]
"@remix-ui/editor-context-view": ["libs/remix-ui/editor-context-view/src/index.ts"],
"@remix-ui/solidity-unit-testing": [
"libs/remix-ui/solidity-unit-testing/src/index.ts"
]
}
},
"exclude": ["node_modules", "tmp"]

@ -1117,6 +1117,24 @@
}
}
},
"solidity-unit-testing": {
"root": "libs/remix-ui/solidity-unit-testing",
"sourceRoot": "libs/remix-ui/solidity-unit-testing/src",
"projectType": "library",
"architect": {
"lint": {
"builder": "@nrwl/linter:lint",
"options": {
"linter": "eslint",
"tsConfig": ["libs/remix-ui/solidity-unit-testing/tsconfig.lib.json"],
"exclude": [
"**/node_modules/**",
"!libs/remix-ui/solidity-unit-testing/**/*"
]
}
}
}
},
"remix-ui-editor-context-view": {
"root": "libs/remix-ui/editor-context-view",
"sourceRoot": "libs/remix-ui/editor-context-view/src",
@ -1169,4 +1187,3 @@
},
"defaultProject": "remix-ide"
}

Loading…
Cancel
Save