diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000000..06cc47d9a2
--- /dev/null
+++ b/.eslintrc.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": {}
+ }
+ ]
+}
diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts
index f6ccce0509..1ca31687c9 100644
--- a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts
+++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts
@@ -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 })
diff --git a/apps/remix-ide/src/app/tabs/test-tab.js b/apps/remix-ide/src/app/tabs/test-tab.js
index c82a2bcc1d..6fd97b446a 100644
--- a/apps/remix-ide/src/app/tabs/test-tab.js
+++ b/apps/remix-ide/src/app/tabs/test-tab.js
@@ -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`
-
- this.toggleCheckbox(e.target.checked, testFile)} type="checkbox" checked="true">
-
-
- `
- }
-
- 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 = `${testName}:\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` this.startDebug(debugTxHash, web3)}>
-
-
`
- 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`
-
- ${this.testSuite} (${this.rawFileName})
-
- `
- 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`
- this.discardHighlight()}
- >
-
- ✓ ${result.value}
- ${debugBtn}
-
-
- `)
- } else if (result.type === 'testFailure') {
- if (result.hhLogs && result.hhLogs.length) this.printHHLogs(result.hhLogs, result.value)
- if (!result.assertMethod) {
- this.testsOutput.appendChild(yo`
- this.highlightLocation(result.location, runningTests, result.filename)}
- >
-
- ✘ ${result.value}
- ${debugBtn}
-
-
Error Message:
-
"${result.errMsg}"
-
- `)
- } 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`
- this.highlightLocation(result.location, runningTests, result.filename)}
- >
-
- ✘ ${result.value}
- ${debugBtn}
-
-
Error Message:
-
"${result.errMsg}"
-
Assertion:
-
-
Expected value should be
-
${method}
-
${preposition} ${expected}
-
-
Received value:
-
${result.returned}
-
Skipping the remaining tests of the function.
-
- `)
- }
- } 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`
-
- PASS
-
- `
-
- this.outputHeader && yo.update(this.outputHeader, yo`
-
- ${label} ${this.testSuite} (${this.rawFileName})
-
- `)
- } else {
- const label = yo`
-
- FAIL
-
- `
-
- this.outputHeader && yo.update(this.outputHeader, yo`
-
- ${label} ${this.testSuite} (${this.rawFileName})
-
- `)
- }
- }
-
- 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`
-
- Result for ${filename}
- Passing: ${result.totalPassing}
- Failing: ${result.totalFailing}
- Total time: ${totalTime}s
-
- `)
- } else if (result.totalPassing > 0 && result.totalFailing <= 0) {
- this.testsOutput.appendChild(yo`
-
- Result for ${filename}
- Passing: ${result.totalPassing}
- Total time: ${totalTime}s
-
- `)
- } else if (result.totalPassing <= 0 && result.totalFailing > 0) {
- this.testsOutput.appendChild(yo`
-
- Result for ${filename}
- Failing: ${result.totalFailing}
- Total time: ${totalTime}s
-
- `)
- }
- // 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`
-
- `)
- }
- 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``)
- }
-
- clearResults () {
- yo.update(this.resultStatistics, yo``)
- 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`
-
- `
- if (!this.generateFileActionElement) {
- this.generateFileActionElement = el
- } else {
- yo.update(this.generateFileActionElement, el)
- }
- return this.generateFileActionElement
- }
-
- updateRunAction (currentFile) {
- const el = yo`
-
- `
- 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`
-
- `
- }
-
- updateTestFileList (tests) {
- const testsMessage = (tests && tests.length ? this.listTests() : 'No test file available')
- const el = yo`${testsMessage}
`
- if (!this.testFilesListElement) {
- this.testFilesListElement = el
- } else {
- yo.update(this.testFilesListElement, el)
- }
- this.updateRunAction()
- return this.testFilesListElement
- }
-
- selectAll () {
- return yo`
-
-
-
-
- `
- }
-
- infoButton () {
- return yo`
-
-
-
- `
- }
-
- createResultLabel () {
- if (!this.data.selectedTests) return yo``
- const ready = this.readyTestsNumber ? `${this.readyTestsNumber}` : '0'
- return yo`Progress: ${ready} finished (of ${this.runningTestsNumber})`
- }
-
- 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``))
- })
- }
-
- 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``
- this.testsExecutionStopped = yo`
`
- this.testsExecutionStoppedError = yo`
`
- this.uiPathList = yo`
`
- this.inputPath = yo`
this.handleTestDirInput(e)}
- onchange=${async (e) => this.handleEnter(e)}
- />`
-
- this.createTestFolder = yo`
-
- `
+ this.renderComponent()
+ return this.element
+ }
- const availablePaths = yo`
-
-
- ${this.inputPath}
- ${this.createTestFolder}
- ${this.uiPathList}
-
-
- `
- this.updateDirList('/')
- this.testsExecutionStopped.hidden = true
- this.testsExecutionStoppedError.hidden = true
- this.resultStatistics = this.createResultLabel()
- this.resultStatistics.hidden = true
- const el = yo`
-
-
-
Test your smart contract in Solidity.
-
Select directory to load and generate test files.
-
- ${availablePaths}
-
-
-
- ${this.updateGenerateFileAction()}
- ${this.infoButton()}
-
-
- ${this.updateRunAction()}
- ${this.updateStopAction()}
-
- ${this.selectAll()}
- ${this.updateTestFileList()}
-
- ${this.resultStatistics}
- ${this.testsExecutionStopped}
- ${this.testsExecutionStoppedError}
-
- ${this.testsOutput}
-
-
- `
- this._view.el = el
- this.testTabLogic.setCurrentPath(this.defaultPath)
- this.updateForNewCurrent(this.fileManager.currentFile())
- return el
+ renderComponent (testDirPath) {
+ ReactDOM.render(
+
+ , this.element)
}
}
diff --git a/apps/remix-ide/src/app/tabs/testTab/testTab.js b/apps/remix-ide/src/app/tabs/testTab/testTab.js
deleted file mode 100644
index 93cb49516b..0000000000
--- a/apps/remix-ide/src/app/tabs/testTab/testTab.js
+++ /dev/null
@@ -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
+ '_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)}";` : '// '
- 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 {
- //
- 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
diff --git a/libs/remix-ui/solidity-unit-testing/.babelrc b/libs/remix-ui/solidity-unit-testing/.babelrc
new file mode 100644
index 0000000000..ccae900be4
--- /dev/null
+++ b/libs/remix-ui/solidity-unit-testing/.babelrc
@@ -0,0 +1,12 @@
+{
+ "presets": [
+ [
+ "@nrwl/react/babel",
+ {
+ "runtime": "automatic",
+ "useBuiltIns": "usage"
+ }
+ ]
+ ],
+ "plugins": []
+}
diff --git a/libs/remix-ui/solidity-unit-testing/.eslintrc.json b/libs/remix-ui/solidity-unit-testing/.eslintrc.json
new file mode 100644
index 0000000000..5e100bf955
--- /dev/null
+++ b/libs/remix-ui/solidity-unit-testing/.eslintrc.json
@@ -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": {}
+ }
+ ]
+}
diff --git a/libs/remix-ui/solidity-unit-testing/README.md b/libs/remix-ui/solidity-unit-testing/README.md
new file mode 100644
index 0000000000..d10d541404
--- /dev/null
+++ b/libs/remix-ui/solidity-unit-testing/README.md
@@ -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).
diff --git a/libs/remix-ui/solidity-unit-testing/src/index.ts b/libs/remix-ui/solidity-unit-testing/src/index.ts
new file mode 100644
index 0000000000..ad00e8d3b4
--- /dev/null
+++ b/libs/remix-ui/solidity-unit-testing/src/index.ts
@@ -0,0 +1,2 @@
+export * from './lib/solidity-unit-testing'
+export * from './lib/logic/testTabLogic'
diff --git a/libs/remix-ui/solidity-unit-testing/src/lib/css/style.css b/libs/remix-ui/solidity-unit-testing/src/lib/css/style.css
new file mode 100644
index 0000000000..f6d05f2150
--- /dev/null
+++ b/libs/remix-ui/solidity-unit-testing/src/lib/css/style.css
@@ -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%;
+ }
\ No newline at end of file
diff --git a/libs/remix-ui/solidity-unit-testing/src/lib/logic/testTabLogic.ts b/libs/remix-ui/solidity-unit-testing/src/lib/logic/testTabLogic.ts
new file mode 100644
index 0000000000..80a65d5f8f
--- /dev/null
+++ b/libs/remix-ui/solidity-unit-testing/src/lib/logic/testTabLogic.ts
@@ -0,0 +1,137 @@
+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.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 (cb: any) {
+ 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: any) {
+ cb(e.message)
+ }
+ for (const 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 + '_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)}";` : '// '
+ 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 {
+ //
+ 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");
+ }
+}
+ `
+ }
+}
diff --git a/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx b/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx
new file mode 100644
index 0000000000..97deaec103
--- /dev/null
+++ b/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx
@@ -0,0 +1,734 @@
+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) => {
+
+ const { helper, testTab, initialPath } = props
+ const { testTabLogic } = testTab
+
+ const [defaultPath, setDefaultPath] = useState('tests')
+ 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([])
+
+ const [testsExecutionStoppedHidden, setTestsExecutionStoppedHidden] = useState(true)
+ const [progressBarHidden, setProgressBarHidden] = useState(true)
+ const [testsExecutionStoppedErrorHidden, setTestsExecutionStoppedErrorHidden] = useState(true)
+
+ let [testFiles, setTestFiles] = useState([]) // 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([])
+
+
+ let areTestsRunning = false
+
+ let runningTestFileName: any
+ const filesContent: any = {}
+ const testsResultByFilename: Record = {}
+
+ 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 {
+ testTabLogic.getTests(async (error: any, tests: any) => {
+ if (error) return setToasterMsg(error)
+ allTests.current = tests
+ selectedTests.current = [...allTests.current]
+ updateTestFileList()
+ if (!areTestsRunning) await updateRunAction(file)
+ })
+ } catch (e) {
+ console.log(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(() => {
+ updateDirList('/')
+ updateForNewCurrent()
+
+ testTab.on('filePanel', 'newTestFileCreated', async (file: string) => {
+ try {
+ testTabLogic.getTests((error: any, tests: any) => {
+ if (error) return setToasterMsg(error)
+ 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 () => {
+ setCurrentPath(defaultPath)
+ })
+
+ testTab.fileManager.events.on('noFileSelected', () => { }) // eslint-disable-line
+ testTab.fileManager.events.on('currentFileChanged', (file: any, provider: any) => 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)
+ 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)
+ testTabLogic.setCurrentPath(testDirInput)
+ updateForNewCurrent()
+ } else {
+ // Enable Create button
+ setDisableCreateButton(false)
+ // Disable Generate button because dir does not exist
+ setDisableGenerateButton(true)
+ }
+ }
+ } else {
+ updateDirList('/')
+ }
+ }
+
+ const handleEnter = async (e: any) => {
+ let inputPath = e.target.value
+ inputPath = helper.removeMultipleSlashes(trimTestDirInput(inputPath))
+ setInputPathValue(inputPath)
+ if (disableCreateButton) {
+ if (await testTabLogic.pathExists(inputPath)) {
+ testTabLogic.setCurrentPath(inputPath)
+ updateForNewCurrent()
+ }
+ }
+ }
+
+ 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()
+ 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 = `${testName}:\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'])
+ 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 = (
+
+ {contract ? contract : ''} ({filename})
+
+ )
+ 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 = (
+ FAIL
+
)
+ } else {
+ const className = "alert-success d-inline-block mb-1 mr-1 p-1 passed_" + runningTestFileName
+ label = (
+ PASS
+
)
+ }
+ // show contract and file name with label
+ const ContractCard: any = (
+
+ {label}{contract} ({filename})
+
+ )
+ 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 = (
+ startDebug(debugTxHash, web3)}>
+
+
+ )
+ }
+ if (test.type === 'testPass') {
+ if (test.hhLogs && test.hhLogs.length) printHHLogs(test.hhLogs, test.value)
+ const testPassCard: any = (
+ discardHighlight()}
+ >
+
+ ✓ {test.value}
+ {debugBtn}
+
+
+ )
+ 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 = ( highlightLocation(test.location, test.filename)}
+ >
+
+ ✘ {test.value}
+ {debugBtn}
+
+
Error Message:
+
"{test.errMsg}"
+
)
+ 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 = ( highlightLocation(test.location, test.filename)}
+ >
+
+ ✘ {test.value}
+ {debugBtn}
+
+
Error Message:
+
"{test.errMsg}"
+
Assertion:
+
+
Expected value should be
+
{method}
+
{preposition} {expected}
+
+
Received value:
+
{test.returned}
+
Skipping the remaining tests of the function.
+
)
+ 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 =
+ setTestsOutput(prevCards => ([...prevCards, errorCard]))
+ })
+ } else if (errors && Array.isArray(errors) && (errors[0].message || errors[0].formattedMessage)) {
+ errors.forEach((err) => {
+ const errorCard: any =
+ 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 =
+ setTestsOutput(prevCards => ([...prevCards, errorCard]))
+ }
+ }
+ }
+ // show summary
+ const testSummary = fileTestsResult['summary']
+ if (testSummary && testSummary.filename && !testSummary.rendered) {
+ const summaryCard: any = (
+ Result for {testSummary.filename}
+ Passed: {testSummary.passed}
+ Failed: {testSummary.failed}
+ Time Taken: {testSummary.timeTaken}s
+
)
+ 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 (
+
+
+
+
Test your smart contract in Solidity.
+
Select directory to load and generate test files.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ { }} // eslint-disable-line
+ />
+
+
+
{testFiles?.length ? testFiles.map((testFileObj: any, index) => {
+ const elemId = `singleTest${testFileObj.fileName}`
+ return (
+
+ toggleCheckbox(e.target.checked, index)} type="checkbox" checked={testFileObj.checked} />
+
+
+ )
+ })
+ : "No test file available"}
+
+ Progress: {readyTestsNumber} finished (of {runningTestsNumber})
+
+
+
+
{testsOutput}
+
+
+ )
+}
+
+export default SolidityUnitTesting
diff --git a/libs/remix-ui/solidity-unit-testing/tsconfig.json b/libs/remix-ui/solidity-unit-testing/tsconfig.json
new file mode 100644
index 0000000000..8bd701c578
--- /dev/null
+++ b/libs/remix-ui/solidity-unit-testing/tsconfig.json
@@ -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"
+ }
+ ]
+}
diff --git a/libs/remix-ui/solidity-unit-testing/tsconfig.lib.json b/libs/remix-ui/solidity-unit-testing/tsconfig.lib.json
new file mode 100644
index 0000000000..b560bc4dec
--- /dev/null
+++ b/libs/remix-ui/solidity-unit-testing/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"]
+}
diff --git a/nx.json b/nx.json
index 5a3da940bf..f2aa009899 100644
--- a/nx.json
+++ b/nx.json
@@ -151,6 +151,9 @@
"remix-ui-theme-module": {
"tags": []
},
+ "solidity-unit-testing": {
+ "tags": []
+ },
"remix-ui-editor-context-view": {
"tags": []
}
diff --git a/package.json b/package.json
index 3aceb8b33d..584a95a3d7 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/tsconfig.base.json b/tsconfig.base.json
index bc3d4dd009..21dfeb4ee8 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -74,7 +74,10 @@
],
"@remix-ui/theme-module": ["libs/remix-ui/theme-module/src/index.ts"],
"@remix-ui/panel": ["libs/remix-ui/panel/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"]
diff --git a/workspace.json b/workspace.json
index d4aca08282..8667298f8d 100644
--- a/workspace.json
+++ b/workspace.json
@@ -1132,6 +1132,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",
@@ -1184,4 +1202,3 @@
},
"defaultProject": "remix-ide"
}
-