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

pull/2035/head
filip mertens 3 years ago
commit e2cbb3b4d2
  1. 10
      .circleci/config.yml
  2. 38
      .eslintrc
  3. 44
      .eslintrc.json
  4. 1
      README.md
  5. 2
      apps/debugger/src/app/debugger-api.ts
  6. 14
      apps/remix-ide-e2e/nightwatch.ts
  7. 14
      apps/remix-ide-e2e/src/commands/addAtAddressInstance.ts
  8. 3
      apps/remix-ide-e2e/src/commands/clickInstance.ts
  9. 6
      apps/remix-ide-e2e/src/commands/createContract.ts
  10. 15
      apps/remix-ide-e2e/src/commands/currentSelectedFileIs.ts
  11. 15
      apps/remix-ide-e2e/src/commands/modalFooterCancelClick.ts
  12. 11
      apps/remix-ide-e2e/src/commands/modalFooterOKClick.ts
  13. 2
      apps/remix-ide-e2e/src/commands/selectContract.ts
  14. 5
      apps/remix-ide-e2e/src/commands/sendLowLevelTx.ts
  15. 41
      apps/remix-ide-e2e/src/commands/signMessage.ts
  16. 9
      apps/remix-ide-e2e/src/commands/testConstantFunction.ts
  17. 1
      apps/remix-ide-e2e/src/commands/validateValueInput.ts
  18. 3
      apps/remix-ide-e2e/src/commands/verifyCallReturnValue.ts
  19. 3
      apps/remix-ide-e2e/src/helpers/init.ts
  20. 2
      apps/remix-ide-e2e/src/local-plugin/src/app/app.tsx
  21. 15
      apps/remix-ide-e2e/src/tests/ballot.test.ts
  22. 12
      apps/remix-ide-e2e/src/tests/ballot_0_4_11.test.ts
  23. 5
      apps/remix-ide-e2e/src/tests/debugger.test.ts
  24. 8
      apps/remix-ide-e2e/src/tests/defaultLayout.test.ts
  25. 235
      apps/remix-ide-e2e/src/tests/editor.spec.ts
  26. 494
      apps/remix-ide-e2e/src/tests/editor.test.ts
  27. 4
      apps/remix-ide-e2e/src/tests/fileExplorer.test.ts
  28. 2
      apps/remix-ide-e2e/src/tests/generalSettings.test.ts
  29. 43
      apps/remix-ide-e2e/src/tests/gist.test.ts
  30. 62
      apps/remix-ide-e2e/src/tests/importFromGithub.test.ts
  31. 7
      apps/remix-ide-e2e/src/tests/libraryDeployment.test.ts
  32. 131
      apps/remix-ide-e2e/src/tests/plugin_api.ts
  33. 10
      apps/remix-ide-e2e/src/tests/publishContract.test.ts
  34. 34
      apps/remix-ide-e2e/src/tests/recorder.test.ts
  35. 4
      apps/remix-ide-e2e/src/tests/remixd.test.ts
  36. 19
      apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts
  37. 6
      apps/remix-ide-e2e/src/tests/solidityImport_group1.spec.ts
  38. 14
      apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts
  39. 1
      apps/remix-ide-e2e/src/tests/specialFunctions.test.ts
  40. 6
      apps/remix-ide-e2e/src/tests/terminal.test.ts
  41. 134
      apps/remix-ide-e2e/src/tests/transactionExecution.test.ts
  42. 2
      apps/remix-ide-e2e/src/tests/txListener.test.ts
  43. 3
      apps/remix-ide-e2e/src/tests/url.spec.ts
  44. 2
      apps/remix-ide-e2e/src/tests/verticalIconsPanel.test.ts
  45. 7
      apps/remix-ide-e2e/src/types/index.d.ts
  46. 2
      apps/remix-ide/.eslintrc
  47. 153
      apps/remix-ide/best-practices.md
  48. 1
      apps/remix-ide/ci/browser_tests_plugin_api.sh
  49. 2
      apps/remix-ide/ci/deploy_from_travis_remix-alpha.sh
  50. 2
      apps/remix-ide/ci/deploy_from_travis_remix-beta.sh
  51. 2
      apps/remix-ide/ci/deploy_from_travis_remix-live.sh
  52. 4
      apps/remix-ide/docs/code_contribution_guide.md
  53. 557
      apps/remix-ide/src/app.js
  54. 31
      apps/remix-ide/src/app/components/hidden-panel.js
  55. 37
      apps/remix-ide/src/app/components/hidden-panel.tsx
  56. 38
      apps/remix-ide/src/app/components/main-panel.js
  57. 57
      apps/remix-ide/src/app/components/main-panel.tsx
  58. 111
      apps/remix-ide/src/app/components/panel.js
  59. 62
      apps/remix-ide/src/app/components/panel.ts
  60. 3
      apps/remix-ide/src/app/components/plugin-manager-component.js
  61. 147
      apps/remix-ide/src/app/components/plugin-manager-settings.js
  62. 156
      apps/remix-ide/src/app/components/side-panel.js
  63. 88
      apps/remix-ide/src/app/components/side-panel.tsx
  64. 355
      apps/remix-ide/src/app/components/vertical-icons.js
  65. 116
      apps/remix-ide/src/app/components/vertical-icons.tsx
  66. 194
      apps/remix-ide/src/app/editor/contextView.js
  67. 10
      apps/remix-ide/src/app/editor/editor.js
  68. 2
      apps/remix-ide/src/app/editor/examples.js
  69. 96
      apps/remix-ide/src/app/files/fileManager.ts
  70. 10
      apps/remix-ide/src/app/files/fileProvider.js
  71. 7
      apps/remix-ide/src/app/panels/file-panel.js
  72. 95
      apps/remix-ide/src/app/panels/layout.ts
  73. 211
      apps/remix-ide/src/app/panels/main-view.js
  74. 18
      apps/remix-ide/src/app/panels/tab-proxy.js
  75. 13
      apps/remix-ide/src/app/panels/terminal.js
  76. 31
      apps/remix-ide/src/app/plugins/config.ts
  77. 44
      apps/remix-ide/src/app/plugins/notification.tsx
  78. 126
      apps/remix-ide/src/app/plugins/permission-handler-plugin.tsx
  79. 143
      apps/remix-ide/src/app/plugins/remixd-handle.tsx
  80. 38
      apps/remix-ide/src/app/state/registry.ts
  81. 10
      apps/remix-ide/src/app/tabs/analysis-tab.js
  82. 32
      apps/remix-ide/src/app/tabs/compile-tab.js
  83. 33
      apps/remix-ide/src/app/tabs/debugger-tab.js
  84. 82
      apps/remix-ide/src/app/tabs/hardhat-provider.js
  85. 128
      apps/remix-ide/src/app/tabs/hardhat-provider.tsx
  86. 22
      apps/remix-ide/src/app/tabs/plugin-tab.js
  87. 423
      apps/remix-ide/src/app/tabs/runTab/contractDropdown.js
  88. 108
      apps/remix-ide/src/app/tabs/runTab/model/dropdownlogic.js
  89. 2
      apps/remix-ide/src/app/tabs/runTab/model/recorder.js
  90. 162
      apps/remix-ide/src/app/tabs/runTab/recorder.js
  91. 449
      apps/remix-ide/src/app/tabs/runTab/settings.js
  92. 4
      apps/remix-ide/src/app/tabs/settings-tab.js
  93. 225
      apps/remix-ide/src/app/tabs/styles/run-tab-styles.js
  94. 764
      apps/remix-ide/src/app/tabs/test-tab.js
  95. 136
      apps/remix-ide/src/app/tabs/testTab/testTab.js
  96. 32
      apps/remix-ide/src/app/tabs/theme-module.js
  97. 29
      apps/remix-ide/src/app/udapp/make-udapp.js
  98. 237
      apps/remix-ide/src/app/udapp/run-tab.js
  99. 212
      apps/remix-ide/src/app/ui/TreeView.js
  100. 212
      apps/remix-ide/src/app/ui/auto-complete-popup.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -181,7 +181,7 @@ jobs:
environment:
- COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
- COMMIT_AUTHOR: "Circle CI"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/assets dist/apps/remix-ide/index.html dist/apps/remix-ide/main.js dist/apps/remix-ide/polyfills.js dist/apps/remix-ide/runtime.js dist/apps/remix-ide/vendor.js dist/apps/remix-ide/favicon.ico"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/assets dist/apps/remix-ide/index.html dist/apps/remix-ide/main.js dist/apps/remix-ide/polyfills.js dist/apps/remix-ide/runtime.js dist/apps/remix-ide/vendor.js dist/apps/remix-ide/favicon.ico dist/apps/remix-ide/vendors~app.js dist/apps/remix-ide/app.js"
working_directory: ~/remix-project
parallelism: 7
steps:
@ -219,7 +219,7 @@ jobs:
environment:
- COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
- COMMIT_AUTHOR: "Circle CI"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/assets dist/apps/remix-ide/production.index.html dist/apps/remix-ide/main.js dist/apps/remix-ide/polyfills.js dist/apps/remix-ide/favicon.ico"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/assets dist/apps/remix-ide/production.index.html dist/apps/remix-ide/main.js dist/apps/remix-ide/polyfills.js dist/apps/remix-ide/favicon.ico dist/apps/remix-ide/vendors~app.js dist/apps/remix-ide/app.js"
working_directory: ~/remix-project
steps:
@ -247,7 +247,7 @@ jobs:
environment:
- COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
- COMMIT_AUTHOR: "Circle CI"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/assets dist/apps/remix-ide/production.index.html dist/apps/remix-ide/main.js dist/apps/remix-ide/polyfills.js dist/apps/remix-ide/favicon.ico"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/assets dist/apps/remix-ide/production.index.html dist/apps/remix-ide/main.js dist/apps/remix-ide/polyfills.js dist/apps/remix-ide/favicon.ico dist/apps/remix-ide/vendors~app.js dist/apps/remix-ide/app.js"
working_directory: ~/remix-project
steps:
@ -273,7 +273,7 @@ jobs:
environment:
- COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
- COMMIT_AUTHOR: "Circle CI"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/assets dist/apps/remix-ide/production.index.html dist/apps/remix-ide/main.js dist/apps/remix-ide/polyfills.js dist/apps/remix-ide/favicon.ico"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/assets dist/apps/remix-ide/production.index.html dist/apps/remix-ide/main.js dist/apps/remix-ide/polyfills.js dist/apps/remix-ide/favicon.ico dist/apps/remix-ide/vendors~app.js dist/apps/remix-ide/app.js"
working_directory: ~/remix-project
steps:
@ -301,7 +301,7 @@ jobs:
environment:
- COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
- COMMIT_AUTHOR: "Circle CI"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/assets dist/apps/remix-ide/production.index.html dist/apps/remix-ide/main.js dist/apps/remix-ide/polyfills.js dist/apps/remix-ide/favicon.ico"
- FILES_TO_PACKAGE: "dist/apps/remix-ide/assets dist/apps/remix-ide/production.index.html dist/apps/remix-ide/main.js dist/apps/remix-ide/polyfills.js dist/apps/remix-ide/favicon.ico dist/apps/remix-ide/vendors~app.js dist/apps/remix-ide/app.js"
working_directory: ~/remix-project
steps:

@ -1,21 +1,21 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"project": "./tsconfig.base.json"
},
"plugins": ["@typescript-eslint", "@nrwl/nx"],
"extends": "standard",
"rules": {
},
"overrides": [
{
"files": ["*.tsx"],
"rules": {
"@typescript-eslint/no-unused-vars": "off"
}
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"project": "./tsconfig.base.json"
},
"plugins": ["@typescript-eslint", "@nrwl/nx"],
"extends": "standard",
"rules": {
},
"overrides": [
{
"files": ["*.tsx"],
"rules": {
"@typescript-eslint/no-unused-vars": "off"
}
]
}
}
]
}

@ -0,0 +1,44 @@
{
"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": {
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/no-empty-function": "off",
"eslint-disable-next-line no-empty": "off",
"no-empty": "off"
}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nrwl/nx/javascript"],
"rules": {}
}
],
"globals": {
"JSX": true
}
}

@ -51,6 +51,7 @@ git clone https://github.com/ethereum/remix-project.git
```bash
cd remix-project
npm install
npm run build:libs // Build remix libs
nx build
nx serve
```

@ -83,7 +83,7 @@ export const DebuggerApiMixin = (Base) => class extends Base {
const target = (address && remixDebug.traceHelper.isContractCreation(address)) ? receipt.contractAddress : address
const targetAddress = target || receipt.contractAddress || receipt.to
const codeAtAddress = await this._web3.eth.getCode(targetAddress)
const output = await this.call('fetchAndCompile', 'resolve', targetAddress, codeAtAddress, 'browser/.debug')
const output = await this.call('fetchAndCompile', 'resolve', targetAddress, codeAtAddress, '.debug')
if (output) {
return new CompilerAbstract(output.languageversion, output.data, output.source)
}

@ -68,7 +68,13 @@ module.exports = {
desiredCapabilities: {
browserName: 'firefox',
javascriptEnabled: true,
acceptSslCerts: true
acceptSslCerts: true,
'moz:firefoxOptions': {
args: [
'-width=2560',
'-height=1440'
]
}
}
},
@ -78,7 +84,11 @@ module.exports = {
javascriptEnabled: true,
acceptSslCerts: true,
'moz:firefoxOptions': {
args: ['-headless']
args: [
'-headless',
'-width=2560',
'-height=1440'
]
}
}
}

@ -2,9 +2,9 @@ import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'
class addAtAddressInstance extends EventEmitter {
command (this: NightwatchBrowser, address: string, isValidFormat: boolean, isValidChecksum: boolean): NightwatchBrowser {
command (this: NightwatchBrowser, address: string, isValidFormat: boolean, isValidChecksum: boolean, isAbi = true): NightwatchBrowser {
this.api.perform((done: VoidFunction) => {
addInstance(this.api, address, isValidFormat, isValidChecksum, () => {
addInstance(this.api, address, isValidFormat, isValidChecksum, isAbi, () => {
done()
this.emit('complete')
})
@ -13,15 +13,19 @@ class addAtAddressInstance extends EventEmitter {
}
}
function addInstance (browser: NightwatchBrowser, address: string, isValidFormat: boolean, isValidChecksum: boolean, callback: VoidFunction) {
function addInstance (browser: NightwatchBrowser, address: string, isValidFormat: boolean, isValidChecksum: boolean, isAbi: boolean, callback: VoidFunction) {
browser.clickLaunchIcon('udapp').clearValue('.ataddressinput').setValue('.ataddressinput', address, function () {
if (!isValidFormat || !isValidChecksum) browser.assert.elementPresent('button[id^="runAndDeployAtAdressButton"]:disabled')
else {
else if (isAbi) {
browser.click('button[id^="runAndDeployAtAdressButton"]')
.waitForElementPresent('[data-id="udappNotify-modal-footer-ok-react"]')
.execute(function () {
const modal = document.querySelector('#modal-footer-ok') as HTMLElement
const modal = document.querySelector('[data-id="udappNotify-modal-footer-ok-react"]') as any
modal.click()
})
} else {
browser.click('button[id^="runAndDeployAtAdressButton"]')
}
callback()
})

@ -3,8 +3,7 @@ import EventEmitter from 'events'
class ClickInstance extends EventEmitter {
command (this: NightwatchBrowser, index: number): NightwatchBrowser {
index = index + 2
const selector = '.instance:nth-of-type(' + index + ') > div > button'
const selector = `[data-id="universalDappUiTitleExpander${index}"]`
this.api.waitForElementPresent(selector).waitForElementContainsText(selector, '', 60000).scrollAndClick(selector).perform(() => { this.emit('complete') })
return this

@ -15,12 +15,12 @@ class CreateContract extends EventEmitter {
function createContract (browser: NightwatchBrowser, inputParams: string, callback: VoidFunction) {
if (inputParams) {
browser.setValue('div[class^="contractActionsContainerSingle"] input', inputParams, function () {
browser.click('#runTabView button[class^="instanceButton"]').pause(500).perform(function () { callback() })
browser.setValue('.udapp_contractActionsContainerSingle > input', inputParams, function () {
browser.click('.udapp_contractActionsContainerSingle > button').pause(500).perform(function () { callback() })
})
} else {
browser
.click('#runTabView button[class^="instanceButton"]')
.click('.udapp_contractActionsContainerSingle > button')
.pause(500)
.perform(function () { callback() })
}

@ -0,0 +1,15 @@
import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'
class CurrentSelectedFileIs extends EventEmitter {
command (this: NightwatchBrowser, value: string): NightwatchBrowser {
this.api
.waitForElementContainsText('*[data-id="tabs-component"] *[data-id="tab-active"]', value)
.perform(() => {
this.emit('complete')
})
return this
}
}
module.exports = CurrentSelectedFileIs

@ -1,14 +1,15 @@
import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'
class ModalFooterOKClick extends EventEmitter {
command (this: NightwatchBrowser): NightwatchBrowser {
this.api.waitForElementVisible('#modal-footer-cancel').perform((client, done) => {
this.api.execute(function () {
const elem = document.querySelector('#modal-footer-cancel') as HTMLElement
class ModalFooterCancelClick extends EventEmitter {
command (this: NightwatchBrowser, id?: string): NightwatchBrowser {
const clientId = id ? `*[data-id="${id}-modal-footer-cancel-react"]` : '#modal-footer-cancel'
this.api.waitForElementVisible(clientId).perform((client, done) => {
this.api.execute(function (clientId) {
const elem = document.querySelector(clientId) as HTMLElement
elem.click()
}, [], () => {
}, [clientId], () => {
done()
this.emit('complete')
})
@ -17,4 +18,4 @@ class ModalFooterOKClick extends EventEmitter {
}
}
module.exports = ModalFooterOKClick
module.exports = ModalFooterCancelClick

@ -2,13 +2,14 @@ import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'
class ModalFooterOKClick extends EventEmitter {
command (this: NightwatchBrowser): NightwatchBrowser {
this.api.waitForElementVisible('#modal-footer-ok').perform((client, done) => {
this.api.execute(function () {
const elem = document.querySelector('#modal-footer-ok') as HTMLElement
command (this: NightwatchBrowser, id?: string): NightwatchBrowser {
const clientId = id ? `*[data-id="${id}-modal-footer-ok-react"]` : '#modal-footer-ok'
this.api.waitForElementVisible(clientId).perform((client, done) => {
this.api.execute(function (clientId) {
const elem = document.querySelector(clientId) as HTMLElement
elem.click()
}, [], () => {
}, [clientId], () => {
done()
this.emit('complete')
})

@ -1,7 +1,7 @@
import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'
const selector = '#runTabView select[class^="contractNames"]'
const selector = '.udapp_contractNames'
class SelectContract extends EventEmitter {
command (this: NightwatchBrowser, contractName: string): NightwatchBrowser {

@ -6,10 +6,11 @@ class sendLowLevelTx extends EventEmitter {
console.log('low level transact to ', address, value, callData)
this.api.waitForElementVisible(`#instance${address} #deployAndRunLLTxSendTransaction`, 1000)
.clearValue(`#instance${address} #deployAndRunLLTxCalldata`)
.setValue(`#instance${address} #deployAndRunLLTxCalldata`, callData)
.sendKeys(`#instance${address} #deployAndRunLLTxCalldata`, ['_', this.api.Keys.BACK_SPACE, callData])
.waitForElementVisible('#value')
.clearValue('#value')
.setValue('#value', value)
.sendKeys('#value', ['1', this.api.Keys.BACK_SPACE, value])
.pause(2000)
.scrollAndClick(`#instance${address} #deployAndRunLLTxSendTransaction`)
.perform(() => {
this.emit('complete')

@ -20,25 +20,28 @@ function signMsg (browser: NightwatchBrowser, msg: string, cb: (hash: { value: s
browser
.waitForElementPresent('i[id="remixRunSignMsg"]')
.click('i[id="remixRunSignMsg"]')
.waitForElementVisible('textarea[id="prompt_text"]')
.setValue('textarea[id="prompt_text"]', msg, () => {
browser.modalFooterOKClick().perform(
(client, done) => {
browser.waitForElementVisible('span[id="remixRunSignMsgHash"]').getText('span[id="remixRunSignMsgHash"]', (v) => { hash = v; done() })
}
)
.perform(
(client, done) => {
browser.waitForElementVisible('span[id="remixRunSignMsgSignature"]').getText('span[id="remixRunSignMsgSignature"]', (v) => { signature = v; done() })
}
)
.modalFooterOKClick()
.perform(
() => {
cb(hash, signature)
}
)
})
.waitForElementVisible('*[data-id="signMessageTextarea"]', 120000)
.click('*[data-id="signMessageTextarea"]')
.setValue('*[data-id="signMessageTextarea"]', msg)
.waitForElementPresent('[data-id="udappNotify-modal-footer-ok-react"]')
.click('[data-id="udappNotify-modal-footer-ok-react"]')
.perform(
(client, done) => {
browser.waitForElementVisible('span[id="remixRunSignMsgHash"]').getText('span[id="remixRunSignMsgHash"]', (v) => { hash = v; done() })
}
)
.perform(
(client, done) => {
browser.waitForElementVisible('span[id="remixRunSignMsgSignature"]').getText('span[id="remixRunSignMsgSignature"]', (v) => { signature = v; done() })
}
)
.waitForElementPresent('[data-id="udappNotify-modal-footer-ok-react"]')
.click('[data-id="udappNotify-modal-footer-ok-react"]')
.perform(
() => {
cb(hash, signature)
}
)
}
module.exports = SelectContract

@ -20,16 +20,17 @@ function testConstantFunction (browser: NightwatchBrowser, address: string, fnFu
document.querySelector('#runTabView').scrollTop = document.querySelector('#runTabView').scrollHeight
}, [], function () {
if (expectedInput) {
client.setValue('#runTabView input[title="' + expectedInput.types + '"]', expectedInput.values)
client.waitForElementPresent('#runTabView input[title="' + expectedInput.types + '"]')
.setValue('#runTabView input[title="' + expectedInput.types + '"]', expectedInput.values)
}
done()
})
})
.click('.instance button[title="' + fnFullName + '"]')
.pause(1000)
.waitForElementPresent('#instance' + address + ' div[class^="contractActionsContainer"] div[class^="value"]')
.scrollInto('#instance' + address + ' div[class^="contractActionsContainer"] div[class^="value"]')
.assert.containsText('#instance' + address + ' div[class^="contractActionsContainer"] div[class^="value"]', expectedOutput).perform(() => {
.waitForElementPresent('#instance' + address + ' .udapp_contractActionsContainer .udapp_value')
.scrollInto('#instance' + address + ' .udapp_contractActionsContainer .udapp_value')
.assert.containsText('#instance' + address + ' .udapp_contractActionsContainer .udapp_value', expectedOutput).perform(() => {
cb()
})
}

@ -6,6 +6,7 @@ class ValidateValueInput extends EventEmitter {
const browser = this.api
browser.perform((done) => {
browser.clearValue(selector)
.pause(2000)
.setValue(selector, valueTosSet)
.execute(function (selector) {
const elem = document.querySelector(selector) as HTMLInputElement

@ -15,7 +15,7 @@ class VerifyCallReturnValue extends EventEmitter {
function verifyCallReturnValue (browser: NightwatchBrowser, address: string, checks: string[], done: VoidFunction) {
browser.execute(function (address: string) {
const nodes = document.querySelectorAll('#instance' + address + ' div[class^="contractActionsContainer"] div[class^="value"]') as NodeListOf<HTMLElement>
const nodes = document.querySelectorAll('#instance' + address + ' [data-id="udapp_value"]') as NodeListOf<HTMLElement>
const ret = []
for (let k = 0; k < nodes.length; k++) {
const text = nodes[k].innerText ? nodes[k].innerText : nodes[k].textContent
@ -23,7 +23,6 @@ function verifyCallReturnValue (browser: NightwatchBrowser, address: string, che
}
return ret
}, [address], function (result) {
console.log('verifyCallReturnValue', result)
for (const k in checks) {
browser.assert.equal(result.value[k].trim(), checks[k].trim())
}

@ -5,10 +5,11 @@ require('dotenv').config()
export default function (browser: NightwatchBrowser, callback: VoidFunction, url?: string, preloadPlugins = true): void {
browser
.url(url || 'http://127.0.0.1:8080')
.pause(5000)
.pause(6000)
.switchBrowserTab(0)
.waitForElementVisible('[id="remixTourSkipbtn"]')
.click('[id="remixTourSkipbtn"]')
.maximizeWindow()
.fullscreenWindow(() => {
if (preloadPlugins) {
initModules(browser, () => {

@ -33,7 +33,7 @@ function App () {
useEffect(() => {
client.onload(async () => {
const customProfiles = ['menuicons', 'tabs', 'solidityUnitTesting']
const customProfiles = ['menuicons', 'tabs', 'solidityUnitTesting', 'hardhat-provider', 'notification']
client.testCommand = async (data: any) => {
console.log(data)

@ -25,7 +25,7 @@ module.exports = {
.setValue('input[placeholder="bytes32[] proposalNames"]', '["0x48656c6c6f20576f726c64210000000000000000000000000000000000000000"]')
.click('*[data-id="Deploy - transact (not payable)"]')
.waitForElementPresent('*[data-id="universalDappUiContractActionWrapper"]', 60000)
.click('*[data-id="universalDappUiTitleExpander"]')
.clickInstance(0)
.clickFunction('delegate - transact (not payable)', { types: 'address to', values: '"0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"' })
.testFunction('last',
{
@ -70,7 +70,7 @@ module.exports = {
.addAtAddressInstance('0x692a70D2e424a56D2C6C27aA97D1a86395877b3A', true, true)
.pause(500)
.waitForElementPresent('*[data-id="universalDappUiContractActionWrapper"]', 60000)
.click('*[data-id="universalDappUiTitleExpander"]')
.clickInstance(0)
.clickFunction('delegate - transact (not payable)', { types: 'address to', values: '"0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"' })
.testFunction('last',
{
@ -84,17 +84,24 @@ module.exports = {
.openFile('Untitled.sol')
.clickLaunchIcon('udapp')
.click('*[data-id="settingsWeb3Mode"]')
.modalFooterOKClick()
.waitForElementPresent('[data-id="envNotification-modal-footer-ok-react"]')
.execute(function () {
const modal = document.querySelector('[data-id="envNotification-modal-footer-ok-react"]') as any
modal.click()
})
.pause(5000)
.execute(function () {
const env: any = document.getElementById('selectExEnvOptions')
return env.value
}, [], function (result) {
console.log({ result })
browser.assert.ok(result.value === 'web3', 'Web3 Provider not selected')
})
.clickLaunchIcon('solidity')
.clickLaunchIcon('udapp')
.pause(2000)
.clearValue('input[placeholder="bytes32[] proposalNames"]')
.setValue('input[placeholder="bytes32[] proposalNames"]', '["0x48656c6c6f20576f726c64210000000000000000000000000000000000000000"]')
.click('*[data-id="Deploy - transact (not payable)"]')
.clickInstance(0)

@ -33,7 +33,7 @@ module.exports = {
.setValue('input[placeholder="uint8 _numProposals"]', '2')
.click('*[data-id="Deploy - transact (not payable)"]')
.waitForElementPresent('*[data-id="universalDappUiContractActionWrapper"]', 60000)
.click('*[data-id="universalDappUiTitleExpander"]')
.clickInstance(0)
.clickFunction('delegate - transact (not payable)', { types: 'address to', values: '"0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"' })
.testFunction('last',
{
@ -65,7 +65,7 @@ module.exports = {
.addAtAddressInstance('0x692a70D2e424a56D2C6C27aA97D1a86395877b3A', true, true)
.pause(500)
.waitForElementPresent('*[data-id="universalDappUiContractActionWrapper"]', 60000)
.click('*[data-id="universalDappUiTitleExpander"]')
.clickInstance(0)
.clickFunction('delegate - transact (not payable)', { types: 'address to', values: '"0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"' })
.testFunction('last',
{
@ -79,10 +79,16 @@ module.exports = {
.openFile('Untitled.sol')
.clickLaunchIcon('udapp')
.click('*[data-id="settingsWeb3Mode"]')
.modalFooterOKClick()
.waitForElementPresent('[data-id="envNotification-modal-footer-ok-react"]')
.execute(function () {
const modal = document.querySelector('[data-id="envNotification-modal-footer-ok-react"]') as any
modal.click()
})
.clickLaunchIcon('solidity')
.clickLaunchIcon('udapp')
.pause(2000)
.clearValue('input[placeholder="uint8 _numProposals"]')
.setValue('input[placeholder="uint8 _numProposals"]', '2')
.click('*[data-id="Deploy - transact (not payable)"]')
.clickInstance(0)

@ -25,8 +25,7 @@ module.exports = {
'Should debug failing transaction #group1': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="verticalIconsKindudapp"]')
.clickLaunchIcon('udapp')
.waitForElementPresent('*[data-id="universalDappUiTitleExpander"]')
.click('*[data-id="universalDappUiTitleExpander"]')
.clickInstance(0)
.scrollAndClick('*[title="string name, uint256 goal"]')
.setValue('*[title="string name, uint256 goal"]', '"toast", 999')
.click('*[data-id="createProject - transact (not payable)"]')
@ -200,7 +199,7 @@ module.exports = {
browser
.addFile('test_jsGetTrace.js', { content: jsGetTrace })
.executeScript('remix.exeCurrent()')
.pause(1000)
.pause(3000)
.waitForElementContainsText('*[data-id="terminalJournal"]', '{"gas":"0x575f","return":"0x0000000000000000000000000000000000000000000000000000000000000000","structLogs":', 60000)
},
// depends on Should debug using generated sources

@ -50,11 +50,15 @@ module.exports = {
'Toggles Terminal': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('div[data-id="terminalContainer"]')
.assert.elementPresent('div[data-id="terminalContainerDisplay"]')
.assert.elementPresent('div[data-id="terminalCli"]')
.assert.elementPresent('div[data-id="terminalContainer"]')
.waitForElementVisible('div[data-id="terminalContainer"]')
.waitForElementVisible('div[data-id="terminalCli"]')
.click('i[data-id="terminalToggleIcon"]')
.checkElementStyle('div[data-id="terminalToggleMenu"]', 'height', '35px')
.assert.not.elementPresent('div[data-id="terminalCli"]')
.click('i[data-id="terminalToggleIcon"]')
.assert.elementPresent('div[data-id="terminalContainerDisplay"]')
.waitForElementVisible('div[data-id="terminalCli"]')
},
'Switch Tabs using tabs icon': function (browser: NightwatchBrowser) {

@ -1,235 +0,0 @@
'use strict'
import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init'
module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done)
},
'Should zoom in editor ': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('div[data-id="mainPanelPluginsContainer"]')
.clickLaunchIcon('filePanel')
.waitForElementVisible('div[data-id="filePanelFileExplorerTree"]')
.openFile('contracts')
.openFile('contracts/1_Storage.sol')
.waitForElementVisible('#editorView')
.checkElementStyle('.view-lines', 'font-size', '14px')
.click('*[data-id="tabProxyZoomIn"]')
.click('*[data-id="tabProxyZoomIn"]')
.checkElementStyle('.view-lines', 'font-size', '16px')
},
'Should zoom out editor ': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.checkElementStyle('.view-lines', 'font-size', '16px')
.click('*[data-id="tabProxyZoomOut"]')
.click('*[data-id="tabProxyZoomOut"]')
.checkElementStyle('.view-lines', 'font-size', '14px')
},
'Should display compile error in editor ': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.setEditorValue(storageContractWithError + 'error')
.pause(2000)
.waitForElementVisible('.margin-view-overlays .fa-exclamation-square', 120000)
.checkAnnotations('fa-exclamation-square', 29) // error
.clickLaunchIcon('udapp')
.checkAnnotationsNotPresent('fa-exclamation-square') // error
.clickLaunchIcon('solidity')
.checkAnnotations('fa-exclamation-square', 29) // error
},
'Should minimize and maximize codeblock in editor ': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.waitForElementVisible('.ace_open')
.click('.ace_start:nth-of-type(1)')
.waitForElementVisible('.ace_closed')
.click('.ace_start:nth-of-type(1)')
.waitForElementVisible('.ace_open')
},
'Should add breakpoint to editor ': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.waitForElementNotPresent('.margin-view-overlays .fa-circle')
.execute(() => {
(window as any).addRemixBreakpoint(1)
}, [], () => {})
.waitForElementVisible('.margin-view-overlays .fa-circle')
},
'Should load syntax highlighter for ace light theme': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.checkElementStyle('.ace_keyword', 'color', aceThemes.light.keyword)
.checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.light.comment)
.checkElementStyle('.ace_function', 'color', aceThemes.light.function)
.checkElementStyle('.ace_variable', 'color', aceThemes.light.variable)
},
'Should load syntax highlighter for ace dark theme': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="verticalIconsKindsettings"]')
.click('*[data-id="verticalIconsKindsettings"]')
.waitForElementVisible('*[data-id="settingsTabThemeLabelDark"]')
.click('*[data-id="settingsTabThemeLabelDark"]')
.pause(2000)
.waitForElementVisible('#editorView')
/* @todo(#2863) ch for class and not colors
.checkElementStyle('.ace_keyword', 'color', aceThemes.dark.keyword)
.checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.dark.comment)
.checkElementStyle('.ace_function', 'color', aceThemes.dark.function)
.checkElementStyle('.ace_variable', 'color', aceThemes.dark.variable)
*/
},
'Should highlight source code ': function (browser: NightwatchBrowser) {
// include all files here because switching between plugins in side-panel removes highlight
browser
.addFile('sourcehighlight.js', sourcehighlightScript)
.addFile('removeAllSourcehighlightScript.js', removeAllSourcehighlightScript)
.openFile('sourcehighlight.js')
.executeScript('remix.exeCurrent()')
.scrollToLine(32)
.waitForElementPresent('.highlightLine33', 60000)
.checkElementStyle('.highlightLine33', 'background-color', 'rgb(52, 152, 219)')
.scrollToLine(40)
.waitForElementPresent('.highlightLine41', 60000)
.checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)')
.scrollToLine(50)
.waitForElementPresent('.highlightLine51', 60000)
.checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)')
},
'Should remove 1 highlight from source code': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]')
.click('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]')
.pause(2000)
.executeScript('remix.exeCurrent()')
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts"]')
.click('li[data-id="treeViewLitreeViewItemcontracts"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.waitForElementNotPresent('.highlightLine33', 60000)
.checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)')
.checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)')
},
'Should remove all highlights from source code ': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemremoveAllSourcehighlightScript.js"]')
.click('li[data-id="treeViewLitreeViewItemremoveAllSourcehighlightScript.js"]')
.pause(2000)
.executeScript('remix.exeCurrent()')
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.pause(2000)
.waitForElementNotPresent('.highlightLine33', 60000)
.waitForElementNotPresent('.highlightLine41', 60000)
.waitForElementNotPresent('.highlightLine51', 60000)
.end()
}
}
const aceThemes = {
light: {
keyword: 'rgb(147, 15, 128)',
comment: 'rgb(35, 110, 36)',
function: 'rgb(0, 0, 162)',
variable: 'rgb(253, 151, 31)'
},
dark: {
keyword: 'rgb(0, 105, 143)',
comment: 'rgb(85, 85, 85)',
function: 'rgb(0, 174, 239)',
variable: 'rgb(153, 119, 68)'
}
}
const sourcehighlightScript = {
content: `
(async () => {
try {
await remix.call('fileManager', 'open', 'contracts/3_Ballot.sol')
const pos = {
start: {
line: 32,
column: 3
},
end: {
line: 32,
column: 20
}
}
await remix.call('editor', 'highlight', pos, 'contracts/3_Ballot.sol')
const pos2 = {
start: {
line: 40,
column: 3
},
end: {
line: 40,
column: 20
}
}
await remix.call('editor', 'highlight', pos2, 'contracts/3_Ballot.sol')
const pos3 = {
start: {
line: 50,
column: 3
},
end: {
line: 50,
column: 20
}
}
await remix.call('editor', 'highlight', pos3, 'contracts/3_Ballot.sol')
} catch (e) {
console.log(e.message)
}
})()
`
}
const removeAllSourcehighlightScript = {
content: `
(async () => {
try {
await remix.call('editor', 'discardHighlight')
} catch (e) {
console.log(e.message)
}
})()
`
}
const storageContractWithError = `
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/**
* @title Storage
* @dev Store & retrieve value in a variable
*/
contract Storage {
uint256 number;
/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num;
}
/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256){
return number;
}
}`

@ -0,0 +1,494 @@
'use strict'
import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init'
module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done, 'http://127.0.0.1:8080', true)
},
'Should zoom in editor #group1': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('div[data-id="mainPanelPluginsContainer"]')
.clickLaunchIcon('filePanel')
.waitForElementVisible('div[data-id="filePanelFileExplorerTree"]')
.openFile('contracts')
.openFile('contracts/1_Storage.sol')
.waitForElementVisible('#editorView')
.checkElementStyle('.view-lines', 'font-size', '14px')
.click('*[data-id="tabProxyZoomIn"]')
.click('*[data-id="tabProxyZoomIn"]')
.checkElementStyle('.view-lines', 'font-size', '16px')
},
'Should zoom out editor #group1': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.checkElementStyle('.view-lines', 'font-size', '16px')
.click('*[data-id="tabProxyZoomOut"]')
.click('*[data-id="tabProxyZoomOut"]')
.checkElementStyle('.view-lines', 'font-size', '14px')
},
'Should display compile error in editor #group1': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.setEditorValue(storageContractWithError + 'error')
.pause(2000)
.waitForElementVisible('.margin-view-overlays .fa-exclamation-square', 120000)
.checkAnnotations('fa-exclamation-square', 29) // error
.clickLaunchIcon('udapp')
.checkAnnotationsNotPresent('fa-exclamation-square') // error
.clickLaunchIcon('solidity')
.checkAnnotations('fa-exclamation-square', 29) // error
},
'Should minimize and maximize codeblock in editor #group1': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.waitForElementVisible('.ace_open')
.click('.ace_start:nth-of-type(1)')
.waitForElementVisible('.ace_closed')
.click('.ace_start:nth-of-type(1)')
.waitForElementVisible('.ace_open')
},
'Should add breakpoint to editor #group1': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.waitForElementNotPresent('.margin-view-overlays .fa-circle')
.execute(() => {
(window as any).addRemixBreakpoint(1)
}, [], () => {})
.waitForElementVisible('.margin-view-overlays .fa-circle')
},
'Should load syntax highlighter for ace light theme #group1': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.checkElementStyle('.ace_keyword', 'color', aceThemes.light.keyword)
.checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.light.comment)
.checkElementStyle('.ace_function', 'color', aceThemes.light.function)
.checkElementStyle('.ace_variable', 'color', aceThemes.light.variable)
},
'Should load syntax highlighter for ace dark theme #group1': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="verticalIconsKindsettings"]')
.click('*[data-id="verticalIconsKindsettings"]')
.waitForElementVisible('*[data-id="settingsTabThemeLabelDark"]')
.click('*[data-id="settingsTabThemeLabelDark"]')
.pause(2000)
.waitForElementVisible('#editorView')
/* @todo(#2863) ch for class and not colors
.checkElementStyle('.ace_keyword', 'color', aceThemes.dark.keyword)
.checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.dark.comment)
.checkElementStyle('.ace_function', 'color', aceThemes.dark.function)
.checkElementStyle('.ace_variable', 'color', aceThemes.dark.variable)
*/
},
'Should highlight source code #group1': function (browser: NightwatchBrowser) {
// include all files here because switching between plugins in side-panel removes highlight
browser
.addFile('sourcehighlight.js', sourcehighlightScript)
.addFile('removeAllSourcehighlightScript.js', removeAllSourcehighlightScript)
.openFile('sourcehighlight.js')
.executeScript('remix.exeCurrent()')
.scrollToLine(32)
.waitForElementPresent('.highlightLine33', 60000)
.checkElementStyle('.highlightLine33', 'background-color', 'rgb(52, 152, 219)')
.scrollToLine(40)
.waitForElementPresent('.highlightLine41', 60000)
.checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)')
.scrollToLine(50)
.waitForElementPresent('.highlightLine51', 60000)
.checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)')
},
'Should remove 1 highlight from source code #group1': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]')
.click('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]')
.pause(2000)
.executeScript('remix.exeCurrent()')
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts"]')
.click('li[data-id="treeViewLitreeViewItemcontracts"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.waitForElementNotPresent('.highlightLine33', 60000)
.checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)')
.checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)')
},
'Should remove all highlights from source code #group1': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemremoveAllSourcehighlightScript.js"]')
.click('li[data-id="treeViewLitreeViewItemremoveAllSourcehighlightScript.js"]')
.pause(2000)
.executeScript('remix.exeCurrent()')
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.pause(2000)
.waitForElementNotPresent('.highlightLine33', 60000)
.waitForElementNotPresent('.highlightLine41', 60000)
.waitForElementNotPresent('.highlightLine51', 60000)
},
'Should display the context view #group2': function (browser: NightwatchBrowser) {
browser
.openFile('contracts')
.openFile('contracts/1_Storage.sol')
.waitForElementVisible('#editorView')
.setEditorValue(storageContractWithError)
.pause(2000)
.execute(() => {
(document.getElementById('editorView') as any).gotoLine(17, 16)
}, [], () => {})
.waitForElementVisible('.contextview')
.waitForElementContainsText('.contextview .type', 'FunctionDefinition')
.waitForElementContainsText('.contextview .name', 'store')
.execute(() => {
(document.getElementById('editorView') as any).gotoLine(18, 12)
}, [], () => {})
.waitForElementContainsText('.contextview .type', 'uint256')
.waitForElementContainsText('.contextview .name', 'number')
.click('.contextview [data-action="previous"]') // declaration
.pause(1000)
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '180')
})
.click('.contextview [data-action="next"]') // back to the initial state
.pause(1000)
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '323')
})
.click('.contextview [data-action="next"]') // next reference
.pause(1000)
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '489')
})
.click('.contextview [data-action="gotoref"]') // back to the declaration
.pause(1000)
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '180')
})
},
'Should display the context view, loop over "Owner" by switching file #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('solidity')
.click('[for="autoCompile"]') // disable auto compile
.openFile('contracts')
.openFile('contracts/3_Ballot.sol')
.waitForElementVisible('#editorView')
.setEditorValue(BallotWithARefToOwner)
.clickLaunchIcon('solidity')
.click('*[data-id="compilerContainerCompileBtn"]') // compile
.pause(2000)
.execute(() => {
(document.getElementById('editorView') as any).gotoLine(14, 6)
}, [], () => {})
.waitForElementVisible('.contextview')
.waitForElementContainsText('.contextview .type', 'ContractDefinition')
.waitForElementContainsText('.contextview .name', 'Owner')
.click('.contextview [data-action="next"]')
.pause(1000)
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '1061')
})
.click('.contextview [data-action="next"]')
.pause(1000)
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '122')
})
.currentSelectedFileIs('2_Owner.sol') // make sure the current file has been properly changed
.click('.contextview [data-action="next"]')
.pause(1000)
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '211')
})
.click('.contextview [data-action="next"]')
.currentSelectedFileIs('3_Ballot.sol')
.pause(1000)
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '1061')
})
.click('.contextview [data-action="gotoref"]') // go to the declaration
.pause(1000)
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '122')
})
.end()
}
}
const aceThemes = {
light: {
keyword: 'rgb(147, 15, 128)',
comment: 'rgb(35, 110, 36)',
function: 'rgb(0, 0, 162)',
variable: 'rgb(253, 151, 31)'
},
dark: {
keyword: 'rgb(0, 105, 143)',
comment: 'rgb(85, 85, 85)',
function: 'rgb(0, 174, 239)',
variable: 'rgb(153, 119, 68)'
}
}
const sourcehighlightScript = {
content: `
(async () => {
try {
await remix.call('fileManager', 'open', 'contracts/3_Ballot.sol')
const pos = {
start: {
line: 32,
column: 3
},
end: {
line: 32,
column: 20
}
}
await remix.call('editor', 'highlight', pos, 'contracts/3_Ballot.sol')
const pos2 = {
start: {
line: 40,
column: 3
},
end: {
line: 40,
column: 20
}
}
await remix.call('editor', 'highlight', pos2, 'contracts/3_Ballot.sol')
const pos3 = {
start: {
line: 50,
column: 3
},
end: {
line: 50,
column: 20
}
}
await remix.call('editor', 'highlight', pos3, 'contracts/3_Ballot.sol')
} catch (e) {
console.log(e.message)
}
})()
`
}
const removeAllSourcehighlightScript = {
content: `
(async () => {
try {
await remix.call('editor', 'discardHighlight')
} catch (e) {
console.log(e.message)
}
})()
`
}
const storageContractWithError = `
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/**
* @title Storage
* @dev Store & retrieve value in a variable
*/
contract Storage {
uint256 number;
/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num;
}
/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256){
return number;
}
}`
const BallotWithARefToOwner = `
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "./2_Owner.sol";
/**
* @title Ballot
* @dev Implements voting process along with vote delegation
*/
contract Ballot {
Owner c;
struct Voter {
uint weight; // weight is accumulated by delegation
bool voted; // if true, that person already voted
address delegate; // person delegated to
uint vote; // index of the voted proposal
}
struct Proposal {
// If you can limit the length to a certain number of bytes,
// always use one of bytes1 to bytes32 because they are much cheaper
bytes32 name; // short name (up to 32 bytes)
uint voteCount; // number of accumulated votes
}
address public chairperson;
mapping(address => Voter) public voters;
Proposal[] public proposals;
/**
* @dev Create a new ballot to choose one of 'proposalNames'.
* @param proposalNames names of proposals
*/
constructor(bytes32[] memory proposalNames) {
c = new Owner();
chairperson = msg.sender;
voters[chairperson].weight = 1;
for (uint i = 0; i < proposalNames.length; i++) {
// 'Proposal({...})' creates a temporary
// Proposal object and 'proposals.push(...)'
// appends it to the end of 'proposals'.
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
/**
* @dev Give 'voter' the right to vote on this ballot. May only be called by 'chairperson'.
* @param voter address of voter
*/
function giveRightToVote(address voter) public {
require(
msg.sender == chairperson,
"Only chairperson can give right to vote."
);
require(
!voters[voter].voted,
"The voter already voted."
);
require(voters[voter].weight == 0);
voters[voter].weight = 1;
}
/**
* @dev Delegate your vote to the voter 'to'.
* @param to address to which vote is delegated
*/
function delegate(address to) public {
Voter storage sender = voters[msg.sender];
require(!sender.voted, "You already voted.");
require(to != msg.sender, "Self-delegation is disallowed.");
while (voters[to].delegate != address(0)) {
to = voters[to].delegate;
// We found a loop in the delegation, not allowed.
require(to != msg.sender, "Found loop in delegation.");
}
sender.voted = true;
sender.delegate = to;
Voter storage delegate_ = voters[to];
if (delegate_.voted) {
// If the delegate already voted,
// directly add to the number of votes
proposals[delegate_.vote].voteCount += sender.weight;
} else {
// If the delegate did not vote yet,
// add to her weight.
delegate_.weight += sender.weight;
}
}
/**
* @dev Give your vote (including votes delegated to you) to proposal 'proposals[proposal].name'.
* @param proposal index of proposal in the proposals array
*/
function vote(uint proposal) public {
Voter storage sender = voters[msg.sender];
require(sender.weight != 0, "Has no right to vote");
require(!sender.voted, "Already voted.");
sender.voted = true;
sender.vote = proposal;
// If 'proposal' is out of the range of the array,
// this will throw automatically and revert all
// changes.
proposals[proposal].voteCount += sender.weight;
}
/**
* @dev Computes the winning proposal taking all previous votes into account.
* @return winningProposal_ index of winning proposal in the proposals array
*/
function winningProposal() public view
returns (uint winningProposal_)
{
uint winningVoteCount = 0;
for (uint p = 0; p < proposals.length; p++) {
if (proposals[p].voteCount > winningVoteCount) {
winningVoteCount = proposals[p].voteCount;
winningProposal_ = p;
}
}
}
/**
* @dev Calls winningProposal() function to get the index of the winner contained in the proposals array and then
* @return winnerName_ the name of the winner
*/
function winnerName() public view
returns (bytes32 winnerName_)
{
winnerName_ = proposals[winningProposal()].name;
}
}
`

@ -4,7 +4,7 @@ import init from '../helpers/init'
import * as path from 'path'
const testData = {
testFile1: path.resolve(__dirname + '/editor.spec.js'), // eslint-disable-line
testFile1: path.resolve(__dirname + '/editor.test.js'), // eslint-disable-line
testFile2: path.resolve(__dirname + '/fileExplorer.test.js'), // eslint-disable-line
testFile3: path.resolve(__dirname + '/generalSettings.test.js') // eslint-disable-line
}
@ -105,7 +105,7 @@ module.exports = {
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile1)
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile2)
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile3)
.waitForElementVisible('[data-id="treeViewLitreeViewItemeditor.spec.js"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItemeditor.test.js"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItemfileExplorer.test.js"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItemgeneralSettings.test.js"]')
.end()

@ -19,7 +19,7 @@ module.exports = {
browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000)
.waitForElementVisible('*[data-id="settingsTabGenerateContractMetadataLabel"]', 5000)
.verify.elementPresent('[data-id="settingsTabGenerateContractMetadata"]:checked')
.click('*[data-id="verticalIconsFileExplorerIcons"]')
.click('*[data-id="verticalIconsKindfilePanel"]')
.click('[data-id="treeViewLitreeViewItemcontracts"]')
.openFile('contracts/3_Ballot.sol')
.click('*[data-id="verticalIconsKindsolidity"]')

@ -72,30 +72,38 @@ module.exports = {
browser.clickLaunchIcon('home')
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('filePanel')
.scrollAndClick('*[data-id="landingPageImportFromGistButton"]')
.waitForElementVisible('*[data-id="modalDialogModalTitle"]')
.assert.containsText('*[data-id="modalDialogModalTitle"]', 'Load a Gist')
.waitForElementVisible('*[data-id="modalDialogModalBody"]')
.assert.containsText('*[data-id="modalDialogModalBody"]', 'Enter the ID of the Gist or URL you would like to load.')
.waitForElementVisible('*[data-id="modalDialogCustomPromptText"]')
.modalFooterCancelClick()
.click('div[title="home"]')
.waitForElementVisible('button[data-id="landingPageImportFromGistButton"]')
.pause(1000)
.scrollAndClick('button[data-id="landingPageImportFromGistButton"]')
.waitForElementVisible('*[data-id="gisthandlerModalDialogModalTitle-react"]')
.assert.containsText('*[data-id="gisthandlerModalDialogModalTitle-react"]', 'Load a Gist')
.waitForElementVisible('*[data-id="gisthandlerModalDialogModalBody-react"]')
.assert.containsText('*[data-id="gisthandlerModalDialogModalBody-react"]', 'Enter the ID of the Gist or URL you would like to load.')
.waitForElementVisible('*[data-id="modalDialogCustomPromp"]')
.modalFooterCancelClick('gisthandler')
},
'Display Error Message For Invalid Gist ID': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('filePanel')
.scrollAndClick('*[data-id="landingPageImportFromGistButton"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptText"]')
.setValue('*[data-id="modalDialogCustomPromptText"]', testData.invalidGistId)
.modalFooterOKClick()
.waitForElementVisible('*[data-id="modalDialogModalBody"]')
.assert.containsText('*[data-id="modalDialogModalBody"]', 'Not Found')
.modalFooterOKClick()
.waitForElementVisible('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]')
.execute(() => {
(document.querySelector('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]') as any).focus()
}, [], () => {})
.setValue('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]', testData.invalidGistId)
.modalFooterOKClick('gisthandler')
.waitForElementVisible('*[data-id="gisthandlerModalDialogModalBody-react"]')
.assert.containsText('*[data-id="gisthandlerModalDialogModalBody-react"]', 'Not Found')
.modalFooterOKClick('gisthandler')
},
'Display Error Message For Missing Gist Token When Publishing': function (browser: NightwatchBrowser) {
browser
.pause(1000)
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('settings')
.waitForElementVisible('[data-id="settingsTabRemoveGistToken"]')
@ -126,9 +134,12 @@ module.exports = {
.click('[data-id="settingsTabSaveGistToken"]')
.clickLaunchIcon('filePanel')
.scrollAndClick('*[data-id="landingPageImportFromGistButton"]')
.waitForElementVisible('*[data-id="modalDialogCustomPromptText"]')
.setValue('*[data-id="modalDialogCustomPromptText"]', testData.validGistId)
.modalFooterOKClick()
.waitForElementVisible('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]')
.execute(function () {
(document.querySelector('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]') as any).focus()
})
.setValue('*[data-id="gisthandlerModalDialogModalBody-react"] input[data-id="modalDialogCustomPromp"]', testData.validGistId)
.modalFooterOKClick('gisthandler')
.openFile(`gist-${testData.validGistId}/README.txt`)
.waitForElementVisible(`div[title='default_workspace/gist-${testData.validGistId}/README.txt']`)
.assert.containsText(`div[title='default_workspace/gist-${testData.validGistId}/README.txt'] > span`, 'README.txt')

@ -0,0 +1,62 @@
'use strict'
import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init'
const testData = {
validURL: 'https://github.com/OpenZeppelin/openzeppelin-solidity/blob/67bca857eedf99bf44a4b6a0fc5b5ed553135316/contracts/access/Roles.sol',
invalidURL: 'https://github.com/Oppelin/Roles.sol'
}
module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done)
},
'Import from GitHub Modal': function (browser: NightwatchBrowser) {
browser.clickLaunchIcon('home')
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('filePanel')
.click('div[title="home"]')
.waitForElementVisible('button[data-id="landingPageImportFromGitHubButton"]')
.pause(1000)
.scrollAndClick('button[data-id="landingPageImportFromGitHubButton"]')
.waitForElementVisible('*[data-id="homeTabModalDialogModalTitle-react"]')
.assert.containsText('*[data-id="homeTabModalDialogModalTitle-react"]', 'Import from Github')
.waitForElementVisible('*[data-id="homeTabModalDialogModalBody-react"]')
.assert.containsText('*[data-id="homeTabModalDialogModalBody-react"]', 'Enter the github URL you would like to load.')
.waitForElementVisible('*[data-id="homeTabModalDialogCustomPromptText"]')
.refresh()
},
'Display Error Message For Invalid GitHub URL Modal': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.scrollAndClick('*[data-id="landingPageImportFromGitHubButton"]')
.waitForElementVisible('input[data-id="homeTabModalDialogCustomPromptText"]')
.execute(() => {
(document.querySelector('input[data-id="homeTabModalDialogCustomPromptText"]') as any).focus()
}, [], () => {})
.setValue('input[data-id="homeTabModalDialogCustomPromptText"]', testData.invalidURL)
.waitForElementVisible('*[data-id="homeTab-modal-footer-ok-react"]')
.scrollAndClick('[data-id="homeTab-modal-footer-ok-react"]') // submitted
.waitForElementVisible('*[data-shared="tooltipPopup"]')
.assert.containsText('*[data-shared="tooltipPopup"] span', 'not found ' + testData.invalidURL)
},
'Import From Github For Valid URL': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.scrollAndClick('*[data-id="landingPageImportFromGitHubButton"]')
.waitForElementVisible('*[data-id="homeTabModalDialogCustomPromptText"]')
.clearValue('*[data-id="homeTabModalDialogCustomPromptText"]')
.execute(() => {
(document.querySelector('input[data-id="homeTabModalDialogCustomPromptText"]') as any).focus()
}, [], () => {})
.setValue('input[data-id="homeTabModalDialogCustomPromptText"]', testData.validURL)
.waitForElementVisible('*[data-id="homeTab-modal-footer-ok-react"]')
.scrollAndClick('[data-id="homeTab-modal-footer-ok-react"]')
.openFile('github/OpenZeppelin/openzeppelin-solidity/contracts/access/Roles.sol')
.waitForElementVisible("div[title='default_workspace/github/OpenZeppelin/openzeppelin-solidity/contracts/access/Roles.sol'")
.end()
}
}

@ -27,8 +27,7 @@ module.exports = {
console.log('testAutoDeployLib ' + address)
addressRef = address
})
.waitForElementPresent('.instance:nth-of-type(2)')
.click('.instance:nth-of-type(2) > div > button')
.clickInstance(0)
.perform((done) => {
browser.testConstantFunction(addressRef, 'get - call', null, '0:\nuint256: 45').perform(() => {
done()
@ -37,7 +36,6 @@ module.exports = {
},
'Test Manual Deploy Lib': function (browser: NightwatchBrowser) {
console.log('testManualDeployLib')
browser.click('*[data-id="deployAndRunClearInstances"]')
.pause(5000)
.clickLaunchIcon('settings')
@ -104,8 +102,7 @@ function checkDeployShouldSucceed (browser: NightwatchBrowser, address: string,
.getAddressAtPosition(1, (address) => {
addressRef = address
})
.waitForElementPresent('.instance:nth-of-type(3)')
.click('.instance:nth-of-type(3) > div > button')
.clickInstance(1)
.perform(() => {
browser
.testConstantFunction(addressRef, 'get - call', null, '0:\nuint256: 45')

@ -10,7 +10,7 @@ declare global {
const localPluginData: Profile & LocationProfile & ExternalProfile = {
name: 'localPlugin',
displayName: 'Local Plugin',
canActivate: ['dGitProvider', 'flattener', 'solidityUnitTesting', 'udapp'],
canActivate: ['dGitProvider', 'flattener', 'solidityUnitTesting', 'udapp', 'hardhat-provider'],
url: 'http://localhost:2020',
location: 'sidePanel'
}
@ -64,12 +64,16 @@ const clearPayLoad = async (browser: NightwatchBrowser) => {
})
}
const clickButton = async (browser: NightwatchBrowser, buttonText: string) => {
const clickButton = async (browser: NightwatchBrowser, buttonText: string, waitResult: boolean = true) => {
return new Promise((resolve) => {
browser.useXpath().waitForElementVisible(`//*[@data-id='${buttonText}']`).pause(100)
.click(`//*[@data-id='${buttonText}']`, async () => {
await checkForAcceptAndRemember(browser)
browser.waitForElementContainsText('//*[@id="callStatus"]', 'stop').perform(() => resolve(true))
if (waitResult) {
browser.waitForElementContainsText('//*[@id="callStatus"]', 'stop').perform(() => resolve(true))
} else {
resolve(true)
}
})
})
}
@ -82,7 +86,7 @@ const checkForAcceptAndRemember = async function (browser: NightwatchBrowser) {
// @ts-ignore
browser.frame(0, () => { resolve(true) })
} else {
browser.waitForElementVisible('//*[@data-id="permissionHandlerRememberUnchecked"]').click('//*[@data-id="permissionHandlerRememberUnchecked"]').waitForElementVisible('//*[@id="modal-footer-ok"]').click('//*[@id="modal-footer-ok"]', () => {
browser.waitForElementVisible('//*[@data-id="permissionHandlerRememberUnchecked"]').click('//*[@data-id="permissionHandlerRememberUnchecked"]').waitForElementVisible('//*[@data-id="PermissionHandler-modal-footer-ok-react"]').click('//*[@data-id="PermissionHandler-modal-footer-ok-react"]', () => {
// @ts-ignore
browser.frame(0, () => { resolve(true) })
})
@ -103,7 +107,7 @@ const checkForAcceptAndRemember = async function (browser: NightwatchBrowser) {
* @return {Promise}
*/
const clickAndCheckLog = async (browser: NightwatchBrowser, buttonText: string, methodResult: any, eventResult: any, payload: any) => {
const clickAndCheckLog = async (browser: NightwatchBrowser, buttonText: string, methodResult: any, eventResult: any, payload: any, waitResult: boolean = true) => {
if (payload) {
await setPayload(browser, payload)
} else {
@ -112,10 +116,14 @@ const clickAndCheckLog = async (browser: NightwatchBrowser, buttonText: string,
if (methodResult && typeof methodResult !== 'string') { methodResult = JSON.stringify(methodResult) }
if (eventResult && typeof eventResult !== 'string') { eventResult = JSON.stringify(eventResult) }
if (buttonText) {
await clickButton(browser, buttonText)
await clickButton(browser, buttonText, waitResult)
}
if (methodResult) {
await debugValues(browser, 'methods', methodResult)
}
if (eventResult) {
await debugValues(browser, 'events', eventResult)
}
await debugValues(browser, 'methods', methodResult)
await debugValues(browser, 'events', eventResult)
}
const assertPluginIsActive = function (browser: NightwatchBrowser, id: string, shouldBeVisible: boolean) {
@ -149,6 +157,18 @@ module.exports = {
await clickAndCheckLog(browser, 'udapp:getAccounts', '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', null, null)
},
'Should select another provider #group1': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'udapp:setEnvironmentMode', null, null, { context: 'vm', fork: 'berlin' })
await browser
.frameParent()
.useCss()
.clickLaunchIcon('udapp')
.waitForElementContainsText('#selectExEnvOptions option:checked', 'JavaScript VM (Berlin)')
.clickLaunchIcon('localPlugin')
.useXpath()
// @ts-ignore
.frame(0)
},
// context menu item
'Should create context menu item #group1': async function (browser: NightwatchBrowser) {
@ -326,5 +346,100 @@ module.exports = {
'Should get compilationresults #group6': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'solidity:getCompilationResult', 'contracts/1_Storage.sol', null, null)
},
// PROVIDER
'Should switch to hardhat provider (provider plugin) #group8': function (browser: NightwatchBrowser) {
browser
.frameParent()
.useCss()
.clickLaunchIcon('pluginManager')
.scrollAndClick('[data-id="pluginManagerComponentActivateButtonhardhat-provider"]')
.clickLaunchIcon('udapp')
.click('*[data-id="Hardhat Provider"]')
.modalFooterOKClick('hardhatprovider')
.waitForElementContainsText('*[data-id="settingsNetworkEnv"]', 'Custom') // e.g Custom (1337) network
.clickLaunchIcon('localPlugin')
.useXpath()
// @ts-ignore
.frame(0)
.perform(async () => {
const request = {
id: 9999,
jsonrpc: '2.0',
method: 'net_listening',
params: []
}
const result = '{"jsonrpc":"2.0","result":true,"id":9999}'
await clickAndCheckLog(browser, 'hardhat-provider:sendAsync', result, null, request)
})
},
// MODAL
'Should open 2 alert in a row and trigger 2 toaster in between #group9': function (browser: NightwatchBrowser) {
browser
.frameParent()
.useCss()
.addFile('test_modal.js', { content: testModalToasterApi })
.executeScript('remix.execute(\'test_modal.js\')')
.clickLaunchIcon('localPlugin')
.useXpath()
// @ts-ignore
.frame(0)
.perform(async () => {
await clickAndCheckLog(browser, 'notification:toast', null, null, 'message toast from local plugin', false) // create a toast on behalf of the localplugin
await clickAndCheckLog(browser, 'notification:alert', null, null, { message: 'message from local plugin', id: 'test_id_1_local_plugin' }, false) // create an alert on behalf of the localplugin
})
.frameParent()
.useCss()
// check the local plugin notifications
.waitForElementVisible('*[data-id="test_id_1_local_pluginModalDialogModalBody-react"]')
.assert.containsText('*[data-id="test_id_1_local_pluginModalDialogModalBody-react"]', 'message from local plugin')
.modalFooterOKClick('test_id_1_local_plugin')
// check the script runner notifications
.waitForElementVisible('*[data-id="test_id_1_ModalDialogModalBody-react"]')
.assert.containsText('*[data-id="test_id_1_ModalDialogModalBody-react"]', 'message 1')
.modalFooterOKClick('test_id_1_')
.waitForElementVisible('*[data-id="test_id_2_ModalDialogModalBody-react"]')
.assert.containsText('*[data-id="test_id_2_ModalDialogModalBody-react"]', 'message 2')
.modalFooterOKClick('test_id_2_')
.waitForElementVisible('*[data-id="test_id_3_ModalDialogModalBody-react"]')
.modalFooterOKClick('test_id_3_')
.journalLastChildIncludes('default value... ') // check the return value of the prompt
// check the toasters
.waitForElementVisible('*[data-shared="tooltipPopup"]')
.waitForElementContainsText('*[data-shared="tooltipPopup"]', 'message toast from local plugin')
.waitForElementContainsText('*[data-shared="tooltipPopup"]', 'I am a toast')
.waitForElementContainsText('*[data-shared="tooltipPopup"]', 'I am a re-toast')
}
}
const testModalToasterApi = `
// Right click on the script name and hit "Run" to execute
(async () => {
try {
setTimeout(async () => {
console.log('test .. ')
remix.call('notification', 'alert', { message: 'message 1', id: 'test_id_1_' })
remix.call('notification', 'toast', 'I am a toast')
remix.call('notification', 'toast', 'I am a re-toast')
remix.call('notification', 'alert', { message: 'message 2', id: 'test_id_2_' })
const modalContent = {
id: 'test_id_3_',
title: 'test with input title',
message: 'test with input content',
modalType: 'prompt',
okLabel: 'OK',
cancelLabel: 'Cancel',
defaultValue: 'default value... '
}
const result = await remix.call('notification', 'modal', modalContent)
console.log(result)
}, 500)
} catch (e) {
console.log(e.message)
}
})() `

@ -60,12 +60,14 @@ module.exports = {
.click('*[data-id="contractDropdownIpfsCheckbox"]')
.waitForElementVisible('*[data-id="Deploy - transact (not payable)"]')
.click('*[data-id="Deploy - transact (not payable)"]')
.pause(8000)
.getModalBody((value, done) => {
.pause(5000)
.waitForElementVisible('[data-id="udappModalDialogModalBody-react"]')
.getText('[data-id="udappModalDialogModalBody-react"]', (result) => {
const value = typeof result.value === 'string' ? result.value : null
if (value.indexOf('Metadata of "storage" was published successfully.') === -1) browser.assert.fail('ipfs deploy failed')
done()
})
.modalFooterOKClick()
.modalFooterOKClick('udapp')
},
'Should remember choice after page refresh': function (browser: NightwatchBrowser) {

@ -17,16 +17,14 @@ module.exports = {
.pause(5000)
.clickLaunchIcon('udapp')
.selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') // this account will be used for this test suite
.click('div[class^="cardContainer"] i[class^="arrow"]')
.click('#runTabView .runtransaction')
.waitForElementPresent('.instance:nth-of-type(2)')
.click('.instance:nth-of-type(2) > div > button')
.waitForElementPresent('.instance:nth-of-type(3)')
.click('.instance:nth-of-type(3) > div > button')
.click('[data-id="udapp_arrow"]')
.click('[data-id="runtransaction"]')
.clickInstance(0)
.clickInstance(1)
.clickFunction('getInt - call')
.clickFunction('getAddress - call')
.clickFunction('getFromLib - call')
.waitForElementPresent('div[class^="contractActionsContainer"] div[class^="value"] ul')
.waitForElementPresent('[data-id="udapp_value"]')
.getAddressAtPosition(1, (address) => {
console.log('Test Recorder ' + address)
addressRef = address
@ -39,12 +37,15 @@ module.exports = {
.testContracts('testRecorder.sol', sources[0]['testRecorder.sol'], ['testRecorder'])
.clickLaunchIcon('udapp')
.createContract('12')
.waitForElementPresent('.instance:nth-of-type(2)')
.click('.instance:nth-of-type(2) > div > button')
.clickInstance(0)
.clickFunction('set - transact (not payable)', { types: 'uint256 _p', values: '34' })
.click('i.savetransaction')
.modalFooterOKClick()
.pause(2000)
.waitForElementVisible('[data-id="udappNotify-modal-footer-ok-react"]')
.execute(function () {
const modalOk = document.querySelector('[data-id="udappNotify-modal-footer-ok-react"]') as any
modalOk.click()
})
.getEditorValue(function (result) {
const parsed = JSON.parse(result)
browser.assert.equal(JSON.stringify(parsed.transactions[0].record.parameters), JSON.stringify(scenario.transactions[0].record.parameters))
@ -74,11 +75,16 @@ module.exports = {
.pause(1000)
.createContract('')
.click('i.savetransaction')
.modalFooterOKClick()
.waitForElementVisible('[data-id="udappNotify-modal-footer-ok-react"]')
.execute(function () {
const modalOk = document.querySelector('[data-id="udappNotify-modal-footer-ok-react"]') as any
modalOk.click()
})
.click('*[data-id="deployAndRunClearInstances"]') // clear udapp
.click('*[data-id="terminalClearConsole"]') // clear terminal
.click('#runTabView .runtransaction')
.clickInstance(1)
.click('[data-id="runtransaction"]')
.clickInstance(2)
.pause(1000)
.clickFunction('set2 - transact (not payable)', { types: 'uint256 _po', values: '10' })
.testFunction('last',

@ -125,9 +125,9 @@ function startRemixd (browser: NightwatchBrowser) {
.clickLaunchIcon('filePanel')
.clickLaunchIcon('pluginManager')
.scrollAndClick('#pluginManager *[data-id="pluginManagerComponentActivateButtonremixd"]')
.waitForElementVisible('#modal-footer-ok', 2000)
.waitForElementVisible('*[data-id="remixdConnect-modal-footer-ok-react"]', 2000)
.pause(2000)
.click('#modal-footer-ok')
.click('*[data-id="remixdConnect-modal-footer-ok-react"]')
// .click('*[data-id="workspacesModalDialog-modal-footer-ok-react"]')
}

@ -36,15 +36,19 @@ module.exports = {
.pause(2000)
.click('*[data-id="settingsRemixRunSignMsg"]')
.pause(2000)
.waitForElementVisible('*[data-id="modalDialogCustomPromptText"]', 120000)
.setValue('*[data-id="modalDialogCustomPromptText"]', 'Remix is cool!')
.waitForElementVisible('*[data-id="signMessageTextarea"]', 120000)
.click('*[data-id="signMessageTextarea"]')
.setValue('*[data-id="signMessageTextarea"]', 'Remix is cool!')
.assert.elementNotPresent('*[data-id="settingsRemixRunSignMsgHash"]')
.assert.elementNotPresent('*[data-id="settingsRemixRunSignMsgSignature"]')
.modalFooterOKClick()
.waitForElementVisible('*[data-id="modalDialogContainer"]', 12000)
.pause(2000)
.waitForElementPresent('[data-id="udappNotify-modal-footer-ok-react"]')
.click('[data-id="udappNotify-modal-footer-ok-react"]')
.waitForElementVisible('*[data-id="udappNotifyModalDialogModalBody-react"]', 12000)
.assert.elementPresent('*[data-id="settingsRemixRunSignMsgHash"]')
.assert.elementPresent('*[data-id="settingsRemixRunSignMsgSignature"]')
.modalFooterOKClick()
.waitForElementPresent('[data-id="udappNotify-modal-footer-ok-react"]')
.click('[data-id="udappNotify-modal-footer-ok-react"]')
},
'Should deploy contract on JavascriptVM #group3': function (browser: NightwatchBrowser) {
@ -63,8 +67,7 @@ module.exports = {
'Should run low level interaction (fallback function) #group3': function (browser: NightwatchBrowser) {
browser.waitForElementPresent('*[data-id="remixIdeSidePanel"]')
.waitForElementPresent('*[data-id="universalDappUiTitleExpander"]')
.click('*[data-id="universalDappUiTitleExpander"]')
.clickInstance(0)
.waitForElementPresent('*[data-id="pluginManagerSettingsDeployAndRunLLTxSendTransaction"]')
.click('*[data-id="pluginManagerSettingsDeployAndRunLLTxSendTransaction"]')
.pause(5000)
@ -180,7 +183,7 @@ module.exports = {
.useCss().switchBrowserTab(0)
.refresh()
.clickLaunchIcon('pluginManager') // load debugger and source verification
// .scrollAndClick('#pluginManager article[id="remixPluginManagerListItem_source-verification"] button')
// .scrollAndClick('#pluginManager article[id="remixPluginManagerListItem_sourcify"] button')
// debugger already activated .scrollAndClick('#pluginManager article[id="remixPluginManagerListItem_debugger"] button')
.clickLaunchIcon('udapp')
.waitForElementPresent('*[data-id="settingsSelectEnvOptions"]')

@ -1,6 +0,0 @@
'use strict'
import * as test from './solidityImport.test'
import buildGroupTest from '../helpers/buildgrouptest'
const group = 'group1'
module.exports = buildGroupTest(group, test)

@ -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 })

@ -173,6 +173,7 @@ module.exports = {
.waitForElementVisible('#value')
.clearValue('#value')
.setValue('#value', '0')
.pause(2000)
.createContract('')
.clickInstance(1)
.pause(1000)

@ -51,7 +51,7 @@ module.exports = {
.click('*[data-id="terminalClearConsole"]') // clear the terminal
.clickLaunchIcon('udapp')
.click('*[data-id="settingsWeb3Mode"]')
.modalFooterOKClick()
.modalFooterOKClick('envNotification')
.executeScript('web3.eth.getAccounts()')
.waitForElementContainsText('*[data-id="terminalJournal"]', '["', 60000) // we check if an array is present, don't need to check for the content
.waitForElementContainsText('*[data-id="terminalJournal"]', '"]', 60000)
@ -109,10 +109,10 @@ module.exports = {
.waitForElementContainsText('*[data-id="terminalJournal"]', 'Contract Address:', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', '0xd9145CCE52D386f254917e481eB44e9943F39138', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', 'Deployment successful.', 60000)
.addAtAddressInstance('0xd9145CCE52D386f254917e481eB44e9943F39138', true, true)
.addAtAddressInstance('0xd9145CCE52D386f254917e481eB44e9943F39138', true, true, false)
.click('*[data-id="terminalClearConsole"]') // clear the terminal
.waitForElementPresent('*[data-id="universalDappUiContractActionWrapper"]', 60000)
.click('*[data-id="universalDappUiTitleExpander"]')
.clickInstance(0)
.clickFunction('changeOwner - transact (not payable)', { types: 'address newOwner', values: '0xd9145CCE52D386f254917e481eB44e9943F39138' }) // execute the "changeOwner" function
.waitForElementContainsText('*[data-id="terminalJournal"]', 'previousOwner', 60000) // check that the script is logging the event
.waitForElementContainsText('*[data-id="terminalJournal"]', '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', 60000) // check that the script is logging the event

@ -15,11 +15,8 @@ module.exports = {
browser.testContracts('Untitled.sol', sources[0]['Untitled.sol'], ['TestContract'])
.clickLaunchIcon('udapp')
.selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') // this account will be used for this test suite
.click('#runTabView button[class^="instanceButton"]')
.waitForElementPresent('.instance:nth-of-type(2)')
.click('.instance:nth-of-type(2) > div > button')
.click('#runTabView .instance div[class^="title"]')
.click('#runTabView .instance div[class^="title"]')
.click('.udapp_contractActionsContainerSingle > button')
.clickInstance(0)
.clickFunction('f - transact (not payable)')
.testFunction('last',
{
@ -45,9 +42,8 @@ module.exports = {
'Test Complex Return Values #group1': function (browser: NightwatchBrowser) {
browser.testContracts('returnValues.sol', sources[1]['returnValues.sol'], ['testReturnValues'])
.clickLaunchIcon('udapp')
.click('#runTabView button[class^="instanceButton"]')
.waitForElementPresent('.instance:nth-of-type(2)')
.click('.instance:nth-of-type(2) > div > button')
.click('.udapp_contractActionsContainerSingle > button')
.clickInstance(0)
.clickFunction('retunValues1 - transact (not payable)')
.testFunction('last',
{
@ -90,9 +86,8 @@ module.exports = {
'Test Complex Input Values #group2': function (browser: NightwatchBrowser) {
browser.testContracts('inputValues.sol', sources[2]['inputValues.sol'], ['test'])
.clickLaunchIcon('udapp')
.click('#runTabView button[class^="instanceButton"]')
.waitForElementPresent('.instance:nth-of-type(2)')
.click('.instance:nth-of-type(2) > div > button')
.click('.udapp_contractActionsContainerSingle > button')
.clickInstance(0)
.clickFunction('inputValue1 - transact (not payable)', { types: 'uint256 _u, int256 _i, string _str', values: '"2343242", "-4324324", "string _ string _ string _ string _ string _ string _ string _ string _ string _ string _"' })
.testFunction('last',
{
@ -136,8 +131,8 @@ module.exports = {
browser.testContracts('eventFunctionInput.sol', sources[3]['eventFunctionInput.sol'], ['C'])
.clickLaunchIcon('udapp')
.selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') // this account will be used for this test suite
.click('#runTabView button[class^="instanceButton"]')
.waitForElementPresent('.instance:nth-of-type(2)')
.click('.udapp_contractActionsContainerSingle > button')
.clickInstance(0)
.click('*[data-id="deployAndRunClearInstances"]')
},
@ -145,7 +140,7 @@ module.exports = {
browser.testContracts('customError.sol', sources[4]['customError.sol'], ['C'])
.clickLaunchIcon('udapp')
.selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') // this account will be used for this test suite
.click('#runTabView button[class^="instanceButton"]')
.click('.udapp_contractActionsContainerSingle > button')
.clickInstance(0)
.clickFunction('g - transact (not payable)')
.pause(5000)
@ -168,7 +163,7 @@ module.exports = {
.clearTransactions()
.click('*[data-id="settingsVMLondonMode"]') // switch to London fork
.selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') // this account will be used for this test suite
.click('#runTabView button[class^="instanceButton"]')
.click('.udapp_contractActionsContainerSingle > button')
.clickInstance(0)
.clickFunction('g - transact (not payable)')
.journalLastChildIncludes('Error provided by the contract:')
@ -186,7 +181,7 @@ module.exports = {
'Should Compile and Deploy a contract which define a custom error in a library, the error should be logged in the terminal #group3': function (browser: NightwatchBrowser) {
browser.testContracts('customErrorLib.sol', sources[5]['customErrorLib.sol'], ['D'])
.clickLaunchIcon('udapp')
.click('#runTabView button[class^="instanceButton"]')
.click('.udapp_contractActionsContainerSingle > button')
.clickInstance(1)
.clickFunction('h - transact (not payable)')
.pause(5000)
@ -200,6 +195,26 @@ module.exports = {
.journalLastChildIncludes('"documentation": "param2 from library"')
.journalLastChildIncludes('"documentation": "param3 from library"')
.journalLastChildIncludes('Debug the transaction to get more information.')
},
'Should compile and deploy 2 simple contracts, the contract creation component state should be correctly reset for the deployment of the second contract #group4': function (browser: NightwatchBrowser) {
browser
.addFile('Storage.sol', sources[6]['Storage.sol'])
.addFile('Owner.sol', sources[6]['Owner.sol'])
.clickLaunchIcon('udapp')
.createContract('42')
.openFile('Storage.sol')
.clickLaunchIcon('udapp')
.createContract('') // this creation will fail if the component hasn't been properly reset.
.clickInstance(1)
.clickFunction('store - transact (not payable)', { types: 'uint256 num', values: '24' })
.testFunction('last', // we check if the contract is actually reachable.
{
status: 'true Transaction mined and execution succeed',
'decoded input': {
'uint256 num': '24'
}
})
.end()
}
}
@ -327,5 +342,92 @@ contract C {
}
}`
}
},
{
'Owner.sol': {
content: `
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/**
* @title Owner
* @dev Set & change owner
*/
contract Owner {
address private owner;
// event for EVM logging
event OwnerSet(address indexed oldOwner, address indexed newOwner);
// modifier to check if caller is owner
modifier isOwner() {
// If the first argument of 'require' evaluates to 'false', execution terminates and all
// changes to the state and to Ether balances are reverted.
// This used to consume all gas in old EVM versions, but not anymore.
// It is often a good idea to use 'require' to check if functions are called correctly.
// As a second argument, you can also provide an explanation about what went wrong.
require(msg.sender == owner, "Caller is not owner");
_;
}
/**
* @dev Set contract deployer as owner
*/
constructor(uint p) {
owner = msg.sender; // 'msg.sender' is sender of current call, contract deployer for a constructor
emit OwnerSet(address(0), owner);
}
/**
* @dev Change owner
* @param newOwner address of new owner
*/
function changeOwner(address newOwner) public isOwner {
emit OwnerSet(owner, newOwner);
owner = newOwner;
}
/**
* @dev Return owner address
* @return address of owner
*/
function getOwner() external view returns (address) {
return owner;
}
}`
},
'Storage.sol': {
content: `
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/**
* @title Storage
* @dev Store & retrieve value in a variable
*/
contract Storage {
uint256 number;
/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num;
}
/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256){
return number;
}
}`
}
}
]

@ -26,7 +26,7 @@ module.exports = {
.setValue('input[placeholder="bytes32[] proposalNames"]', '["0x48656c6c6f20576f726c64210000000000000000000000000000000000000000"]')
.click('*[data-id="Deploy - transact (not payable)"]')
.waitForElementPresent('*[data-id="universalDappUiContractActionWrapper"]', 60000)
.click('*[data-id="universalDappUiTitleExpander"]')
.clickInstance(0)
.clickFunction('delegate - transact (not payable)', { types: 'address to', values: '"0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"' })
.testFunction('last',
{

@ -77,12 +77,13 @@ module.exports = {
'Should load using URL compiler params': function (browser: NightwatchBrowser) {
browser
.pause(5000)
.url('http://127.0.0.1:8080/#optimize=true&runs=300&autoCompile=true&evmVersion=istanbul&version=soljson-v0.7.4+commit.3f05b770.js')
.url('http://127.0.0.1:8080/#optimize=true&runs=300&autoCompile=true&evmVersion=istanbul&version=soljson-v0.7.4+commit.3f05b770.js&language=Yul')
.refresh()
.pause(5000)
.clickLaunchIcon('solidity')
.assert.containsText('#versionSelector option[data-id="selected"]', '0.7.4+commit.3f05b770')
.assert.containsText('#evmVersionSelector option[data-id="selected"]', 'istanbul')
.assert.containsText('#compilierLanguageSelector option[data-id="selected"]', 'Yul')
.verify.elementPresent('#optimize:checked')
.verify.elementPresent('#autoCompile:checked')
.verify.attributeEquals('#runs', 'value', '300')

@ -27,6 +27,6 @@ module.exports = {
.click('*[id="menuitemdeactivate"]')
.click('*[data-id="verticalIconsKindsettings"]')
.click('*[data-id="verticalIconsKindpluginManager"]')
.waitForElementVisible('*[data-id="pluginManagerComponentActivateButtondebugPlugin"]')
.waitForElementVisible('*[data-id="pluginManagerComponentActivateButtondebugger"]')
}
}

@ -17,8 +17,8 @@ declare module 'nightwatch' {
testFunction(txHash: string, expectedInput: NightwatchTestFunctionExpectedInput): NightwatchBrowser,
goToVMTraceStep(step: number, incr?: number): NightwatchBrowser,
checkVariableDebug(id: string, debugValue: NightwatchCheckVariableDebugValue): NightwatchBrowser,
addAtAddressInstance(address: string, isValidFormat: boolean, isValidChecksum: boolean): NightwatchBrowser,
modalFooterOKClick(): NightwatchBrowser,
addAtAddressInstance(address: string, isValidFormat: boolean, isValidChecksum: boolean, isAbi?: boolean): NightwatchBrowser,
modalFooterOKClick(id?: string): NightwatchBrowser,
clickInstance(index: number): NightwatchBrowser,
journalLastChildIncludes(val: string): NightwatchBrowser,
executeScript(script: string): NightwatchBrowser,
@ -32,7 +32,7 @@ declare module 'nightwatch' {
scrollToLine(line: number): NightwatchBrowser,
waitForElementContainsText(id: string, value: string, timeout?: number): NightwatchBrowser,
getModalBody(callback: (value: string, cb: VoidFunction) => void): NightwatchBrowser,
modalFooterCancelClick(): NightwatchBrowser,
modalFooterCancelClick(id?: string): NightwatchBrowser,
selectContract(contractName: string): NightwatchBrowser,
createContract(inputParams: string): NightwatchBrowser,
getAddressAtPosition(index: number, cb: (pos: string) => void): NightwatchBrowser,
@ -61,6 +61,7 @@ declare module 'nightwatch' {
acceptAndRemember (this: NightwatchBrowser, remember: boolean, accept: boolean): NightwatchBrowser
clearConsole (this: NightwatchBrowser): NightwatchBrowser
clearTransactions (this: NightwatchBrowser): NightwatchBrowser
currentSelectedFileIs (name: string): NightwatchBrowser
}
export interface NightwatchBrowser {

@ -3,7 +3,7 @@
"browser": true,
"es6": true
},
"extends": "../../.eslintrc",
"extends": "../../.eslintrc.json",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"

@ -1,153 +0,0 @@
# Current "Best Practice" Conventions
- Please use [JS Standard Style](https://standardjs.com/) as a coding style guide.
- `ES6 class` rather than ES5 to create class.
- CSS declaration using `csjs-inject`.
- CSS files:
 **if** the CSS section of an UI component is too important, CSS declarations should be put in a different file and in a different folder.
The folder should be named `styles` and the file should be named with the extension `-styles.css`.
 e.g: `file-explorer.js` being an UI component `file-explorer-styles.css` is created in the `styles` folder right under `file-explorer.js`
**if** the CSS section of an UI component is rather limited it is preferable to put it in the corresponding JS file.
- HTML declaration using `yo-yo`.
- A module trigger events using `event` property:
 `self.event = new EventManager()`.
Events can then be triggered:
 `self.event.trigger('eventName', [param1, param2])`
- `self._view` is the HTML view renderered by `yo-yo` in the `render` function.
- `render()` this function should be called at the first rendering (make sure that the returned node element is put on the DOM), and should *not* by called again from outside the component.
- `update()` call this function to update the DOM when the state of the component has changed (this function must be called after the initial call to `render()`).
- for all functions / properties, prefixing by underscore (`_`) means the scope is private, and they should **not** be accessed not changed from outside the component.
- constructor arguments: There is no fixed rule whether it is preferrable to use multiples arguments or a single option *{}* argument (or both).
We recommend:
- use a specific slot for **obligatory** arguments and/or for complex arguments (meaning not boolean, not string, etc...).
- put arguments in an option *{}* for non critical and for optionnal arguments.
- if a component has more than 4/5 parameters, it is recommended to find a way to group some in one or more *opt* arguments.
- look them up, discuss them, update them.
   
## Module Definition (example)
```js
// user-card.js
var yo = require('yo-yo')
var csjs = require('csjs-inject')
var EventManager = require('remix-lib').EventManager
var css = csjs`
.userCard {
position : relative;
box-sizing : border-box;
display : flex;
flex-direction : column;
align-items : center;
border : 1px solid black;
width : 400px;
padding : 50px;
}
.clearFunds { background-color: lightblue; }
`
class UserCard {
constructor (api, events, opts = {}) {
var self = this
self.event = new EventManager()
self.opts = opts
self._api = api
self._consumedEvents = events
self._view = undefined
events.funds.register('fundsChanged', function (amount) {
if (amount < self.state._funds) self.state.totalSpend += self.state._funds - amount
self.state._funds = amount
self.render()
})
self.event.trigger('eventName', [param1, param2])
}
render () {
var self = this
var view = yo`
<div class=${css.userCard}>
<h1> @${self.state._nickname} </h1>
<h2> Welcome, ${self.state.title || ''} ${self.state.name || 'anonymous'} ${self.state.surname} </h2>
<ul> <li> User Funds: $${self.state._funds} </li> </ul>
<ul> <li> Spent Funds: $${self.state.totalSpend} </li> </ul>
<button class=${css.clearFunds} onclick=${e=>self._spendAll.call(self, e)}> spend all funds </button>
</div>
`
if (!self._view) {
self._view = view
}
return self._view
}
update () {
yo.update(this._view, this.render())
}
setNickname (name) {
this._nickname = name
}
getNickname () {
var self = this
return `@${self.state._nickname}`
}
getFullName () {
var self = this
return `${self.state.title} ${self.state.name} ${self.state.surname}`
}
_spendAll (event) {
var self = this
self._appAPI.clearUserFunds()
}
_constraint (msg) { throw new Error(msg) }
}
module.exports = UserCard
```
## Module Usage (example)
```js
/*****************************************************************************/
// 1. SETUP CONTEXT
var EventManager = require('remix-lib').EventManager
var funds = { event: new EventManager() }
var userfunds = 15
function getUserFunds () { return userfunds }
function clearUserFunds () {
var spent = userfunds
userfunds = 0
console.log(`all funds of ${usercard.getFullName()} were spent.`)
funds.event.trigger('fundsChanged', [userfunds])
return spent
}
setInterval(function () {
userfunds++
funds.event.trigger('fundsChanged', [userfunds])
}, 100)
/*****************************************************************************/
// 2. EXAMPLE USAGE
var UserCard = require('./user-card')
var usercard = new UserCard(
{ getUserFunds, clearUserFunds },
{ funds: funds.event },
{
title: 'Dr.',
name: 'John',
surname: 'Doe',
nickname: 'johndoe99'
})
var el = usercard.render()
document.body.appendChild(el)
setTimeout(function () {
userCard.setNickname('new name')
usercard.update()
}, 5000)
```

@ -6,6 +6,7 @@ BUILD_ID=${CIRCLE_BUILD_NUM:-${TRAVIS_JOB_NUMBER}}
echo "$BUILD_ID"
TEST_EXITCODE=0
npm run ganache-cli &
npm run serve:production &
npx nx serve remix-ide-e2e-src-local-plugin &

@ -15,7 +15,7 @@ cp -r $FILES_TO_PACKAGE "./"
rm -rf dist
ls
mv production.index.html index.html
FILES_TO_DEPLOY="assets index.html main.js polyfills.js favicon.ico"
FILES_TO_DEPLOY="assets index.html main.js polyfills.js favicon.ico vendors~app.js app.js"
# ZIP the whole directory
zip -r remix-$SHA.zip $FILES_TO_DEPLOY
# -f is needed because "build" is part of .gitignore

@ -15,7 +15,7 @@ cp -r $FILES_TO_PACKAGE "./"
rm -rf dist
ls
mv production.index.html index.html
FILES_TO_DEPLOY="assets index.html main.js polyfills.js favicon.ico"
FILES_TO_DEPLOY="assets index.html main.js polyfills.js favicon.ico vendors~app.js app.js"
# ZIP the whole directory
zip -r remix-$SHA.zip $FILES_TO_DEPLOY
# -f is needed because "build" is part of .gitignore

@ -15,7 +15,7 @@ cp -r $FILES_TO_PACKAGE "./"
rm -rf dist
ls
mv production.index.html index.html
FILES_TO_DEPLOY="assets index.html main.js polyfills.js"
FILES_TO_DEPLOY="assets index.html main.js polyfills.js vendors~app.js app.js"
# ZIP the whole directory
zip -r remix-$SHA.zip $FILES_TO_DEPLOY
# -f is needed because "build" is part of .gitignore

@ -5,7 +5,7 @@ Remix is an open source tool and we encourage anyone to help us improve our tool
You can do that by opening issues, giving feedback or by contributing a pull request
to our codebase.
The Remix application is built with JavaScript and it doesn't use any framework. We only
rely on selected set of npm modules, like `yo-yo`, `csjs-inject` and others. Check out the `package.json` files in the Remix submodules to learn more about the stack.
The Remix application is built with JavaScript and React.
Check out the `package.json` files in the Remix submodules to learn more about the stack.
To learn more, please visit our [GitHub page](https://github.com/ethereum/remix-ide).

@ -1,14 +1,7 @@
'use strict'
import { basicLogo } from './app/ui/svgLogo'
import { RunTab, makeUdapp } from './app/udapp'
import PanelsResize from './lib/panels-resize'
import { RemixEngine } from './remixEngine'
import { RemixAppManager } from './remixAppManager'
import { FramingService } from './framingService'
import { WalkthroughService } from './walkthroughService'
import { MainView } from './app/panels/main-view'
import { ThemeModule } from './app/tabs/theme-module'
import { NetworkModule } from './app/tabs/network-module'
import { Web3ProviderModule } from './app/tabs/web3-provider'
@ -17,27 +10,32 @@ import { HiddenPanel } from './app/components/hidden-panel'
import { VerticalIcons } from './app/components/vertical-icons'
import { LandingPage } from './app/ui/landing-page/landing-page'
import { MainPanel } from './app/components/main-panel'
import { OffsetToLineColumnConverter, CompilerMetadata, CompilerArtefacts, FetchAndCompile, CompilerImports } from '@remix-project/core-plugin'
import { PermissionHandlerPlugin } from './app/plugins/permission-handler-plugin'
import { WalkthroughService } from './walkthroughService'
import { OffsetToLineColumnConverter, CompilerMetadata, CompilerArtefacts, FetchAndCompile, CompilerImports, EditorContextListener, GistHandler } from '@remix-project/core-plugin'
import Registry from './app/state/registry'
import { ConfigPlugin } from './app/plugins/config'
import { Layout } from './app/panels/layout'
import { NotificationPlugin } from './app/plugins/notification'
import { Blockchain } from './blockchain/blockchain.js'
import { HardhatProvider } from './app/tabs/hardhat-provider'
const isElectron = require('is-electron')
const csjs = require('csjs-inject')
const yo = require('yo-yo')
const remixLib = require('@remix-project/remix-lib')
const registry = require('./global/registry')
const QueryParams = require('./lib/query-params')
const Storage = remixLib.Storage
const RemixDProvider = require('./app/files/remixDProvider')
const HardhatProvider = require('./app/tabs/hardhat-provider')
const Config = require('./config')
const modalDialogCustom = require('./app/ui/modal-dialog-custom')
const modalDialog = require('./app/ui/modaldialog')
const FileManager = require('./app/files/fileManager')
const FileProvider = require('./app/files/fileProvider')
const DGitProvider = require('./app/files/dgitProvider')
const WorkspaceFileProvider = require('./app/files/workspaceFileProvider')
const toolTip = require('./app/ui/tooltip')
const Blockchain = require('./blockchain/blockchain.js')
const PluginManagerComponent = require('./app/components/plugin-manager-component')
@ -49,172 +47,71 @@ const TestTab = require('./app/tabs/test-tab')
const FilePanel = require('./app/panels/file-panel')
const Editor = require('./app/editor/editor')
const Terminal = require('./app/panels/terminal')
const ContextualListener = require('./app/editor/contextualListener')
const _paq = window._paq = window._paq || []
const css = csjs`
html { box-sizing: border-box; }
*, *:before, *:after { box-sizing: inherit; }
body {
/* font: 14px/1.5 Lato, "Helvetica Neue", Helvetica, Arial, sans-serif; */
font-size : .8rem;
}
pre {
overflow-x: auto;
}
.remixIDE {
width : 100vw;
height : 100vh;
overflow : hidden;
flex-direction : row;
display : flex;
}
.mainpanel {
display : flex;
flex-direction : column;
overflow : hidden;
flex : 1;
}
.iconpanel {
display : flex;
flex-direction : column;
overflow : hidden;
width : 50px;
user-select : none;
}
.sidepanel {
display : flex;
flex-direction : row-reverse;
width : 320px;
}
.highlightcode {
position : absolute;
z-index : 20;
background-color : var(--info);
}
.highlightcode_fullLine {
position : absolute;
z-index : 20;
background-color : var(--info);
opacity : 0.5;
}
.centered {
position : fixed;
top : 20%;
left : 45%;
width : 200px;
height : 200px;
}
.centered svg path {
fill: var(--secondary);
}
.centered svg polygon {
fill : var(--secondary);
}
.onboarding {
color : var(--text-info);
background-color : var(--info);
}
.matomoBtn {
width : 100px;
}
`
const { TabProxy } = require('./app/panels/tab-proxy.js')
class App {
constructor (api = {}, events = {}, opts = {}) {
var self = this
class AppComponent {
constructor () {
const self = this
self.appManager = new RemixAppManager({})
self.queryParams = new QueryParams()
self._components = {}
self._view = {}
self._view.splashScreen = yo`
<div class=${css.centered}>
${basicLogo()}
<div class="info-secondary" style="text-align:center">
REMIX IDE
</div>
</div>
`
document.body.appendChild(self._view.splashScreen)
// setup storage
const configStorage = new Storage('config-v0.8:')
// load app config
const config = new Config(configStorage)
registry.put({ api: config, name: 'config' })
Registry.getInstance().put({ api: config, name: 'config' })
// load file system
self._components.filesProviders = {}
self._components.filesProviders.browser = new FileProvider('browser')
registry.put({ api: self._components.filesProviders.browser, name: 'fileproviders/browser' })
self._components.filesProviders.localhost = new RemixDProvider(self.appManager)
registry.put({ api: self._components.filesProviders.localhost, name: 'fileproviders/localhost' })
Registry.getInstance().put({
api: self._components.filesProviders.browser,
name: 'fileproviders/browser'
})
self._components.filesProviders.localhost = new RemixDProvider(
self.appManager
)
Registry.getInstance().put({
api: self._components.filesProviders.localhost,
name: 'fileproviders/localhost'
})
self._components.filesProviders.workspace = new WorkspaceFileProvider()
registry.put({ api: self._components.filesProviders.workspace, name: 'fileproviders/workspace' })
registry.put({ api: self._components.filesProviders, name: 'fileproviders' })
}
init () {
this.run().catch(console.error)
}
Registry.getInstance().put({
api: self._components.filesProviders.workspace,
name: 'fileproviders/workspace'
})
render () {
var self = this
if (self._view.el) return self._view.el
// not resizable
self._view.iconpanel = yo`
<div id="icon-panel" data-id="remixIdeIconPanel" class="${css.iconpanel} bg-light">
${''}
</div>
`
// center panel, resizable
self._view.sidepanel = yo`
<div id="side-panel" data-id="remixIdeSidePanel" style="min-width: 320px;" class="${css.sidepanel} border-right border-left">
${''}
</div>
`
// handle the editor + terminal
self._view.mainpanel = yo`
<div id="main-panel" data-id="remixIdeMainPanel" class=${css.mainpanel}>
${''}
</div>
`
self._components.resizeFeature = new PanelsResize(self._view.sidepanel)
self._view.el = yo`
<div style="visibility:hidden" class=${css.remixIDE} data-id="remixIDE">
${self._view.iconpanel}
${self._view.sidepanel}
${self._components.resizeFeature.render()}
${self._view.mainpanel}
</div>
`
return self._view.el
Registry.getInstance().put({
api: self._components.filesProviders,
name: 'fileproviders'
})
}
async run () {
var self = this
// check the origin and warn message
if (window.location.hostname === 'yann300.github.io') {
modalDialogCustom.alert('This UNSTABLE ALPHA branch of Remix has been moved to http://ethereum.github.io/remix-live-alpha.')
} else if (window.location.hostname === 'remix-alpha.ethereum.org' ||
(window.location.hostname === 'ethereum.github.io' && window.location.pathname.indexOf('/remix-live-alpha') === 0)) {
modalDialogCustom.alert('Welcome to the Remix alpha instance. Please use it to try out latest features. But use preferably https://remix.ethereum.org for any production work.')
} else if (window.location.protocol.indexOf('http') === 0 &&
window.location.hostname !== 'remix.ethereum.org' &&
window.location.hostname !== 'localhost' &&
window.location.hostname !== '127.0.0.1') {
modalDialogCustom.alert(`The Remix IDE has moved to http://remix.ethereum.org.\n
This instance of Remix you are visiting WILL NOT BE UPDATED.\n
Please make a backup of your contracts and start using http://remix.ethereum.org`)
}
if (window.location.protocol.indexOf('https') === 0) {
toolTip('You are using an `https` connection. Please switch to `http` if you are using Remix against an `http Web3 provider` or allow Mixed Content in your browser.')
const self = this
// APP_MANAGER
const appManager = self.appManager
const pluginLoader = self.appManager.pluginLoader
self.panels = {}
self.workspace = pluginLoader.get()
self.engine = new RemixEngine()
self.engine.register(appManager)
const matomoDomains = {
'remix-alpha.ethereum.org': 27,
'remix-beta.ethereum.org': 25,
'remix.ethereum.org': 23
}
self.showMatamo =
matomoDomains[window.location.hostname] &&
!Registry.getInstance()
.get('config')
.api.exists('settings/matomo-analytics')
self.walkthroughService = new WalkthroughService(
appManager,
self.showMatamo
)
const hosts = ['127.0.0.1:8080', '192.168.0.101:8080', 'localhost:8080']
// workaround for Electron support
@ -225,44 +122,39 @@ class App {
}
}
// APP_MANAGER
const appManager = self.appManager
const pluginLoader = appManager.pluginLoader
const workspace = pluginLoader.get()
const engine = new RemixEngine()
engine.register(appManager)
// SERVICES
// ----------------- gist service ---------------------------------
self.gistHandler = new GistHandler()
// ----------------- theme service ---------------------------------
const themeModule = new ThemeModule(registry)
registry.put({ api: themeModule, name: 'themeModule' })
themeModule.initTheme(() => {
setTimeout(() => {
document.body.removeChild(self._view.splashScreen)
self._view.el.style.visibility = 'visible'
}, 1500)
})
self.themeModule = new ThemeModule()
Registry.getInstance().put({ api: self.themeModule, name: 'themeModule' })
// ----------------- editor service ----------------------------
const editor = new Editor() // wrapper around ace editor
registry.put({ api: editor, name: 'editor' })
editor.event.register('requiringToSaveCurrentfile', () => fileManager.saveCurrentFile())
Registry.getInstance().put({ api: editor, name: 'editor' })
editor.event.register('requiringToSaveCurrentfile', () =>
fileManager.saveCurrentFile()
)
// ----------------- fileManager service ----------------------------
const fileManager = new FileManager(editor, appManager)
registry.put({ api: fileManager, name: 'filemanager' })
Registry.getInstance().put({ api: fileManager, name: 'filemanager' })
// ----------------- dGit provider ---------------------------------
const dGitProvider = new DGitProvider()
// ----------------- import content service ------------------------
const contentImport = new CompilerImports()
const blockchain = new Blockchain(registry.get('config').api)
const blockchain = new Blockchain(Registry.getInstance().get('config').api)
// ----------------- compilation metadata generation service ---------
const compilerMetadataGenerator = new CompilerMetadata()
// ----------------- compilation result service (can keep track of compilation results) ----------------------------
const compilersArtefacts = new CompilerArtefacts() // store all the compilation results (key represent a compiler name)
registry.put({ api: compilersArtefacts, name: 'compilersartefacts' })
Registry.getInstance().put({
api: compilersArtefacts,
name: 'compilersartefacts'
})
// service which fetch contract artifacts from sourve-verify, put artifacts in remix and compile it
const fetchAndCompile = new FetchAndCompile()
@ -273,29 +165,44 @@ class App {
const hardhatProvider = new HardhatProvider(blockchain)
// ----------------- convert offset to line/column service -----------
const offsetToLineColumnConverter = new OffsetToLineColumnConverter()
registry.put({ api: offsetToLineColumnConverter, name: 'offsettolinecolumnconverter' })
Registry.getInstance().put({
api: offsetToLineColumnConverter,
name: 'offsettolinecolumnconverter'
})
// -------------------Terminal----------------------------------------
makeUdapp(blockchain, compilersArtefacts, (domEl) => terminal.logHtml(domEl))
makeUdapp(blockchain, compilersArtefacts, domEl => terminal.logHtml(domEl))
const terminal = new Terminal(
{ appManager, blockchain },
{
getPosition: (event) => {
var limitUp = 36
var limitDown = 20
var height = window.innerHeight
var newpos = (event.pageY < limitUp) ? limitUp : event.pageY
newpos = (newpos < height - limitDown) ? newpos : height - limitDown
getPosition: event => {
const limitUp = 36
const limitDown = 20
const height = window.innerHeight
let newpos = event.pageY < limitUp ? limitUp : event.pageY
newpos = newpos < height - limitDown ? newpos : height - limitDown
return height - newpos
}
}
)
const contextualListener = new ContextualListener({ editor })
const contextualListener = new EditorContextListener()
engine.register([
self.notification = new NotificationPlugin()
const configPlugin = new ConfigPlugin()
self.layout = new Layout()
const permissionHandler = new PermissionHandlerPlugin()
self.engine.register([
permissionHandler,
self.layout,
self.notification,
self.gistHandler,
configPlugin,
blockchain,
contentImport,
themeModule,
self.themeModule,
editor,
fileManager,
compilerMetadataGenerator,
@ -307,135 +214,76 @@ class App {
web3Provider,
fetchAndCompile,
dGitProvider,
hardhatProvider
hardhatProvider,
self.walkthroughService
])
// LAYOUT & SYSTEM VIEWS
const appPanel = new MainPanel()
const mainview = new MainView(contextualListener, editor, appPanel, fileManager, appManager, terminal)
registry.put({ api: mainview, name: 'mainview' })
engine.register([
appPanel,
mainview.tabProxy
])
Registry.getInstance().put({ api: self.mainview, name: 'mainview' })
const tabProxy = new TabProxy(fileManager, editor)
self.engine.register([appPanel, tabProxy])
// those views depend on app_manager
const menuicons = new VerticalIcons(appManager)
const sidePanel = new SidePanel(appManager, menuicons)
const hiddenPanel = new HiddenPanel()
const pluginManagerComponent = new PluginManagerComponent(appManager, engine)
self.menuicons = new VerticalIcons()
self.sidePanel = new SidePanel()
self.hiddenPanel = new HiddenPanel()
const pluginManagerComponent = new PluginManagerComponent(
appManager,
self.engine
)
const filePanel = new FilePanel(appManager)
const landingPage = new LandingPage(appManager, menuicons, fileManager, filePanel, contentImport)
const settings = new SettingsTab(
registry.get('config').api,
const landingPage = new LandingPage(
appManager,
self.menuicons,
fileManager,
filePanel,
contentImport
)
self.settings = new SettingsTab(
Registry.getInstance().get('config').api,
editor,
appManager
)
// adding Views to the DOM
self._view.mainpanel.appendChild(mainview.render())
self._view.iconpanel.appendChild(menuicons.render())
self._view.sidepanel.appendChild(sidePanel.render())
document.body.appendChild(hiddenPanel.render()) // Hidden Panel is display none, it can be directly on body
engine.register([
menuicons,
self.engine.register([
self.menuicons,
landingPage,
hiddenPanel,
sidePanel,
self.hiddenPanel,
self.sidePanel,
filePanel,
pluginManagerComponent,
settings
self.settings
])
const queryParams = new QueryParams()
const params = queryParams.get()
const onAcceptMatomo = () => {
_paq.push(['forgetUserOptOut'])
// @TODO remove next line when https://github.com/matomo-org/matomo/commit/9e10a150585522ca30ecdd275007a882a70c6df5 is used
document.cookie = 'mtm_consent_removed=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'
settings.updateMatomoAnalyticsChoice(true)
const el = document.getElementById('modal-dialog')
el.parentElement.removeChild(el)
startWalkthroughService()
}
const onDeclineMatomo = () => {
settings.updateMatomoAnalyticsChoice(false)
_paq.push(['optUserOut'])
const el = document.getElementById('modal-dialog')
el.parentElement.removeChild(el)
startWalkthroughService()
}
const startWalkthroughService = () => {
const walkthroughService = new WalkthroughService(localStorage)
if (!params.code && !params.url && !params.minimizeterminal && !params.gist && !params.minimizesidepanel) {
walkthroughService.start()
}
}
// Ask to opt in to Matomo for remix, remix-alpha and remix-beta
const matomoDomains = {
'remix-alpha.ethereum.org': 27,
'remix-beta.ethereum.org': 25,
'remix.ethereum.org': 23
}
if (matomoDomains[window.location.hostname] && !registry.get('config').api.exists('settings/matomo-analytics')) {
modalDialog(
'Help us to improve Remix IDE',
yo`
<div>
<p>An Opt-in version of <a href="https://matomo.org" target="_blank">Matomo</a>, an open source data analytics platform is being used to improve Remix IDE.</p>
<p>We realize that our users have sensitive information in their code and that their privacy - your privacy - must be protected.</p>
<p>All data collected through Matomo is stored on our own server - no data is ever given to third parties. Our analytics reports are public: <a href="https://matomo.ethereum.org/index.php?module=MultiSites&action=index&idSite=23&period=day&date=yesterday" target="_blank">take a look</a>.</p>
<p>We do not collect nor store any personally identifiable information (PII).</p>
<p>For more info, see: <a href="https://medium.com/p/66ef69e14931/" target="_blank">Matomo Analyitcs on Remix iDE</a>.</p>
<p>You can change your choice in the Settings panel anytime.</p>
<div class="d-flex justify-content-around pt-3 border-top">
<button class="btn btn-primary ${css.matomoBtn}" onclick=${() => onAcceptMatomo()}>Sure</button>
<button class="btn btn-secondary ${css.matomoBtn}" onclick=${() => onDeclineMatomo()}>Decline</button>
</div>
</div>`,
{
label: '',
fn: null
},
{
label: '',
fn: null
}
)
} else {
startWalkthroughService()
}
// CONTENT VIEWS & DEFAULT PLUGINS
const compileTab = new CompileTab(registry.get('config').api, registry.get('filemanager').api)
const compileTab = new CompileTab(
Registry.getInstance().get('config').api,
Registry.getInstance().get('filemanager').api
)
const run = new RunTab(
blockchain,
registry.get('config').api,
registry.get('filemanager').api,
registry.get('editor').api,
Registry.getInstance().get('config').api,
Registry.getInstance().get('filemanager').api,
Registry.getInstance().get('editor').api,
filePanel,
registry.get('compilersartefacts').api,
Registry.getInstance().get('compilersartefacts').api,
networkModule,
mainview,
registry.get('fileproviders/browser').api
Registry.getInstance().get('fileproviders/browser').api
)
const analysis = new AnalysisTab(registry)
const analysis = new AnalysisTab()
const debug = new DebuggerTab()
const test = new TestTab(
registry.get('filemanager').api,
registry.get('offsettolinecolumnconverter').api,
Registry.getInstance().get('filemanager').api,
Registry.getInstance().get('offsettolinecolumnconverter').api,
filePanel,
compileTab,
appManager,
contentImport
)
engine.register([
self.engine.register([
compileTab,
run,
debug,
@ -447,68 +295,91 @@ class App {
filePanel.slitherHandle
])
self.layout.panels = {
tabs: { plugin: tabProxy, active: true },
editor: { plugin: editor, active: true },
main: { plugin: appPanel, active: false },
terminal: { plugin: terminal, active: true, minimized: false }
}
}
async activate () {
const queryParams = new QueryParams()
const params = queryParams.get()
const self = this
if (isElectron()) {
appManager.activatePlugin('remixd')
self.appManager.activatePlugin('remixd')
}
try {
engine.register(await appManager.registeredPlugins())
self.engine.register(await self.appManager.registeredPlugins())
} catch (e) {
console.log('couldn\'t register iframe plugins', e.message)
console.log("couldn't register iframe plugins", e.message)
}
await self.appManager.activatePlugin(['layout'])
await self.appManager.activatePlugin(['notification'])
await self.appManager.activatePlugin(['editor'])
await self.appManager.activatePlugin(['permissionhandler', 'theme', 'fileManager', 'compilerMetadata', 'compilerArtefacts', 'network', 'web3Provider', 'offsetToLineColumnConverter'])
await self.appManager.activatePlugin(['mainPanel', 'menuicons', 'tabs'])
await self.appManager.activatePlugin(['sidePanel']) // activating host plugin separately
await self.appManager.activatePlugin(['home'])
await self.appManager.activatePlugin(['settings', 'config'])
await self.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler'])
await self.appManager.activatePlugin(['settings'])
await self.appManager.activatePlugin(['walkthrough'])
self.appManager.on(
'filePanel',
'workspaceInitializationCompleted',
async () => {
await self.appManager.registerContextMenuItems()
}
)
await appManager.activatePlugin(['editor'])
await appManager.activatePlugin(['theme', 'fileManager', 'compilerMetadata', 'compilerArtefacts', 'network', 'web3Provider', 'offsetToLineColumnConverter'])
await appManager.activatePlugin(['mainPanel', 'menuicons', 'tabs'])
await appManager.activatePlugin(['sidePanel']) // activating host plugin separately
await appManager.activatePlugin(['home'])
await appManager.activatePlugin(['settings'])
await appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport'])
appManager.on('filePanel', 'workspaceInitializationCompleted', async () => {
await appManager.registerContextMenuItems()
})
await appManager.activatePlugin(['filePanel'])
await self.appManager.activatePlugin(['filePanel'])
// Set workspace after initial activation
appManager.on('editor', 'editorMounted', () => {
if (Array.isArray(workspace)) {
appManager.activatePlugin(workspace).then(async () => {
try {
if (params.deactivate) {
await appManager.deactivatePlugin(params.deactivate.split(','))
self.appManager.on('editor', 'editorMounted', () => {
if (Array.isArray(self.workspace)) {
self.appManager
.activatePlugin(self.workspace)
.then(async () => {
try {
if (params.deactivate) {
await self.appManager.deactivatePlugin(
params.deactivate.split(',')
)
}
} catch (e) {
console.log(e)
}
if (params.code) {
// if code is given in url we focus on solidity plugin
self.menuicons.select('solidity')
} else {
// If plugins are loaded from the URL params, we focus on the last one.
if (
self.appManager.pluginLoader.current === 'queryParams' &&
self.workspace.length > 0
) { self.menuicons.select(self.workspace[self.workspace.length - 1]) }
}
} catch (e) {
console.log(e)
}
if (params.code) {
// if code is given in url we focus on solidity plugin
menuicons.select('solidity')
} else {
// If plugins are loaded from the URL params, we focus on the last one.
if (pluginLoader.current === 'queryParams' && workspace.length > 0) menuicons.select(workspace[workspace.length - 1])
}
if (params.call) {
const callDetails = params.call.split('//')
if (callDetails.length > 1) {
toolTip(`initiating ${callDetails[0]} ...`)
// @todo(remove the timeout when activatePlugin is on 0.3.0)
appManager.call(...callDetails).catch(console.error)
if (params.call) {
const callDetails = params.call.split('//')
if (callDetails.length > 1) {
self.appManager.call('notification', 'toast', `initiating ${callDetails[0]} ...`)
// @todo(remove the timeout when activatePlugin is on 0.3.0)
self.appManager.call(...callDetails).catch(console.error)
}
}
}
}).catch(console.error)
})
.catch(console.error)
}
})
// activate solidity plugin
appManager.activatePlugin(['solidity', 'udapp'])
self.appManager.activatePlugin(['solidity', 'udapp'])
// Load and start the service who manager layout and frame
const framingService = new FramingService(sidePanel, menuicons, mainview, this._components.resizeFeature)
if (params.embed) framingService.embed()
framingService.start(params)
}
}
module.exports = App
export default AppComponent

@ -1,31 +0,0 @@
import { AbstractPanel } from './panel'
import * as packageJson from '../../../../../package.json'
const csjs = require('csjs-inject')
const yo = require('yo-yo')
const css = csjs`
.pluginsContainer {
display: none;
}
`
const profile = {
name: 'hiddenPanel',
displayName: 'Hidden Panel',
description: '',
version: packageJson.version,
methods: ['addView', 'removeView']
}
export class HiddenPanel extends AbstractPanel {
constructor () {
super(profile)
}
render () {
return yo`
<div class=${css.pluginsContainer}>
${this.view}
</div>`
}
}

@ -0,0 +1,37 @@
// eslint-disable-next-line no-use-before-define
import React from 'react'
import ReactDOM from 'react-dom' // eslint-disable-line
import { AbstractPanel } from './panel'
import * as packageJson from '../../../../../package.json'
import { RemixPluginPanel } from '@remix-ui/panel'
const profile = {
name: 'hiddenPanel',
displayName: 'Hidden Panel',
description: '',
version: packageJson.version,
methods: ['addView', 'removeView']
}
export class HiddenPanel extends AbstractPanel {
el: HTMLElement
constructor () {
super(profile)
this.el = document.createElement('div')
this.el.setAttribute('class', 'pluginsContainer')
}
addView (profile: any, view: any): void {
super.removeView(profile)
super.addView(profile, view)
this.renderComponent()
}
render () {
return this.el
}
renderComponent () {
ReactDOM.render(<RemixPluginPanel header={<></>} plugins={this.plugins}/>, this.el)
}
}

@ -1,38 +0,0 @@
import { AbstractPanel } from './panel'
import * as packageJson from '../../../../../package.json'
const yo = require('yo-yo')
const csjs = require('csjs-inject')
const css = csjs`
.pluginsContainer {
height: 100%;
display: flex;
overflow-y: hidden;
}
`
const profile = {
name: 'mainPanel',
displayName: 'Main Panel',
description: '',
version: packageJson.version,
methods: ['addView', 'removeView']
}
export class MainPanel extends AbstractPanel {
constructor () {
super(profile)
}
focus (name) {
this.emit('focusChanged', name)
super.focus(name)
}
render () {
return yo`
<div class=${css.pluginsContainer} data-id="mainPanelPluginsContainer" id='mainPanelPluginsContainer-id'>
${this.view}
</div>`
}
}

@ -0,0 +1,57 @@
import React from 'react' // eslint-disable-line
import { AbstractPanel } from './panel'
import ReactDOM from 'react-dom' // eslint-disable-line
import { RemixPluginPanel } from '@remix-ui/panel'
import packageJson from '../../../../../package.json'
const profile = {
name: 'mainPanel',
displayName: 'Main Panel',
description: '',
version: packageJson.version,
methods: ['addView', 'removeView', 'showContent']
}
export class MainPanel extends AbstractPanel {
element: HTMLDivElement
constructor (config) {
super(profile)
this.element = document.createElement('div')
this.element.setAttribute('data-id', 'mainPanelPluginsContainer')
this.element.setAttribute('style', 'height: 100%; width: 100%;')
// this.config = config
}
onActivation () {
this.renderComponent()
}
focus (name) {
this.emit('focusChanged', name)
super.focus(name)
this.renderComponent()
}
addView (profile, view) {
super.addView(profile, view)
this.renderComponent()
}
removeView (profile) {
super.removeView(profile)
this.renderComponent()
}
async showContent (name) {
super.showContent(name)
this.renderComponent()
}
render () {
return this.element
}
renderComponent () {
ReactDOM.render(<RemixPluginPanel header={<></>} plugins={this.plugins}/>, this.element)
}
}

@ -1,111 +0,0 @@
import { EventEmitter } from 'events'
import { HostPlugin } from '@remixproject/engine-web'
const csjs = require('csjs-inject')
const yo = require('yo-yo')
const css = csjs`
.plugins {
height: 100%;
}
.plugItIn {
display : none;
height : 100%;
}
.plugItIn > div {
overflow-y : auto;
overflow-x : hidden;
height : 100%;
width : 100%;
}
.plugItIn.active {
display : block;
}
.pluginsContainer {
height : 100%;
overflow-y : hidden;
}
`
/** Abstract class used for hosting the view of a plugin */
export class AbstractPanel extends HostPlugin {
constructor (profile) {
super(profile)
this.events = new EventEmitter()
this.contents = {}
this.active = undefined
// View where the plugin HTMLElement leaves
this.view = yo`<div id="plugins" class="${css.plugins}"></div>`
}
/**
* Add the plugin to the panel
* @param {String} name the name of the plugin
* @param {HTMLElement} content the HTMLContent of the plugin
*/
add (view, name) {
if (this.contents[name]) throw new Error(`Plugin ${name} already rendered`)
view.style.height = '100%'
view.style.width = '100%'
view.style.border = '0'
const isIframe = view.tagName === 'IFRAME'
view.style.display = isIframe ? 'none' : 'block'
const loading = isIframe ? yo`
<div class="d-flex justify-content-center align-items-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
` : ''
this.contents[name] = yo`<div class="${css.plugItIn}" >${view}${loading}</div>`
if (view.tagName === 'IFRAME') {
view.addEventListener('load', () => {
if (this.contents[name].contains(loading)) {
this.contents[name].removeChild(loading)
}
view.style.display = 'block'
})
}
this.contents[name].style.display = 'none'
this.view.appendChild(this.contents[name])
}
addView (profile, view) {
this.add(view, profile.name)
}
removeView (profile) {
this.remove(profile.name)
}
/**
* Remove a plugin from the panel
* @param {String} name The name of the plugin to remove
*/
remove (name) {
const el = this.contents[name]
delete this.contents[name]
if (el) el.parentElement.removeChild(el)
if (name === this.active) this.active = undefined
}
/**
* Display the content of this specific plugin
* @param {String} name The name of the plugin to display the content
*/
showContent (name) {
if (!this.contents[name]) throw new Error(`Plugin ${name} is not yet activated`)
// hiding the current view and display the `moduleName`
if (this.active) {
this.contents[this.active].style.display = 'none'
}
this.contents[name].style.display = 'flex'
this.active = name
}
focus (name) {
this.showContent(name)
}
}

@ -0,0 +1,62 @@
import React from 'react' // eslint-disable-line
import { EventEmitter } from 'events'
import { HostPlugin } from '@remixproject/engine-web' // eslint-disable-line
import { PluginRecord } from 'libs/remix-ui/panel/src/lib/types'
const EventManager = require('../../lib/events')
export class AbstractPanel extends HostPlugin {
events: EventEmitter
event: any
public plugins: Record<string, PluginRecord> = {}
constructor (profile) {
super(profile)
this.events = new EventEmitter()
this.event = new EventManager()
}
currentFocus (): string {
return Object.values(this.plugins).find(plugin => {
return plugin.active
}).profile.name
}
addView (profile, view) {
if (this.plugins[profile.name]) throw new Error(`Plugin ${profile.name} already rendered`)
this.plugins[profile.name] = {
profile: profile,
view: view,
active: false,
class: 'plugItIn active'
}
}
removeView (profile) {
this.emit('pluginDisabled', profile.name)
this.call('menuicons', 'unlinkContent', profile)
this.remove(profile.name)
}
/**
* Remove a plugin from the panel
* @param {String} name The name of the plugin to remove
*/
remove (name) {
delete this.plugins[name]
}
/**
* Display the content of this specific plugin
* @param {String} name The name of the plugin to display the content
*/
showContent (name) {
if (!this.plugins[name]) throw new Error(`Plugin ${name} is not yet activated`)
Object.values(this.plugins).forEach(plugin => {
plugin.active = false
})
this.plugins[name].active = true
}
focus (name) {
this.showContent(name)
}
}

@ -1,5 +1,4 @@
import { ViewPlugin } from '@remixproject/engine-web'
import { PluginManagerSettings } from './plugin-manager-settings'
import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
import {RemixUiPluginManager} from '@remix-ui/plugin-manager' // eslint-disable-line
@ -24,7 +23,6 @@ class PluginManagerComponent extends ViewPlugin {
super(profile)
this.appManager = appManager
this.engine = engine
this.pluginManagerSettings = new PluginManagerSettings()
this.htmlElement = document.createElement('div')
this.htmlElement.setAttribute('id', 'pluginManager')
this.filter = ''
@ -90,7 +88,6 @@ class PluginManagerComponent extends ViewPlugin {
ReactDOM.render(
<RemixUiPluginManager
pluginComponent={this}
pluginManagerSettings={this.pluginManagerSettings}
/>,
this.htmlElement)
}

@ -1,147 +0,0 @@
const yo = require('yo-yo')
const csjs = require('csjs-inject')
const modalDialog = require('../ui/modaldialog')
const css = csjs`
.remixui_permissions {
position: sticky;
bottom: 0;
display: flex;
justify-content: flex-end;
align-items: center;
padding: 5px 20px;
}
.permissions button {
padding: 2px 5px;
cursor: pointer;
}
.permissionForm h4 {
font-size: 1.3rem;
text-align: center;
}
.permissionForm h6 {
font-size: 1.1rem;
}
.permissionForm hr {
width: 80%;
}
.permissionKey {
display: flex;
justify-content: space-between;
align-items: center;
}
.permissionKey i {
cursor: pointer;
}
.checkbox {
display: flex;
align-items: center;
}
.checkbox label {
margin: 0;
font-size: 1rem;
}
`
export class PluginManagerSettings {
constructor () {
const fromLocal = window.localStorage.getItem('plugins/permissions')
this.permissions = JSON.parse(fromLocal || '{}')
}
openDialog () {
this.currentSetting = this.settings()
modalDialog('Plugin Manager Permissions', this.currentSetting,
{ fn: () => this.onValidation() }
)
}
onValidation () {
const permissions = JSON.stringify(this.permissions)
window.localStorage.setItem('plugins/permissions', permissions)
}
/** Clear one permission from a plugin */
clearPersmission (from, to, method) {
// eslint-disable-next-line no-debugger
debugger
if (this.permissions[to] && this.permissions[to][method]) {
delete this.permissions[to][method][from]
if (Object.keys(this.permissions[to][method]).length === 0) {
delete this.permissions[to][method]
}
if (Object.keys(this.permissions[to]).length === 0) {
delete this.permissions[to]
}
yo.update(this.currentSetting, this.settings())
}
}
/** Clear all persmissions from a plugin */
clearAllPersmission (to) {
// eslint-disable-next-line no-debugger
debugger
if (!this.permissions[to]) return
delete this.permissions[to]
yo.update(this.currentSetting, this.settings())
}
settings () {
const permissionByToPlugin = (toPlugin, funcObj) => {
const permissionByMethod = (methodName, fromPlugins) => {
const togglePermission = (fromPlugin) => {
this.permissions[toPlugin][methodName][fromPlugin].allow = !this.permissions[toPlugin][methodName][fromPlugin].allow
}
return Object.keys(fromPlugins).map(fromName => {
const fromPluginPermission = fromPlugins[fromName]
const checkbox = fromPluginPermission.allow
? yo`<input onchange=${() => togglePermission(fromName)} class="mr-2" type="checkbox" checked id="permission-checkbox-${toPlugin}-${methodName}-${toPlugin}" aria-describedby="module ${fromPluginPermission} asks permission for ${methodName}" />`
: yo`<input onchange=${() => togglePermission(fromName)} class="mr-2" type="checkbox" id="permission-checkbox-${toPlugin}-${methodName}-${toPlugin}" aria-describedby="module ${fromPluginPermission} asks permission for ${methodName}" />`
return yo`
<div class="form-group ${css.permissionKey}">
<div class="${css.checkbox}">
${checkbox}
<label for="permission-checkbox-${toPlugin}-${methodName}-${toPlugin}" data-id="permission-label-${toPlugin}-${methodName}-${toPlugin}">Allow <u>${fromName}</u> to call <u>${methodName}</u></label>
</div>
<i onclick="${() => this.clearPersmission(fromName, toPlugin, methodName)}" class="fa fa-trash-alt" data-id="pluginManagerSettingsRemovePermission-${toPlugin}-${methodName}-${toPlugin}"></i>
</div>
`
})
}
const permissionsByFunctions = Object
.keys(funcObj)
.map(methodName => permissionByMethod(methodName, funcObj[methodName]))
return yo`
<div border p-2>
<div class="pb-2 ${css.permissionKey}">
<h3>${toPlugin} permissions:</h3>
<i onclick="${() => this.clearAllPersmission(toPlugin)}" class="far fa-trash-alt" data-id="pluginManagerSettingsClearAllPermission-${toPlugin}"></i>
</div>
${permissionsByFunctions}
</div>`
}
const byToPlugin = Object
.keys(this.permissions)
.map(toPlugin => permissionByToPlugin(toPlugin, this.permissions[toPlugin]))
const title = byToPlugin.length === 0
? yo`<h4>No Permission requested yet.</h4>`
: yo`<h4>Current Permission settings</h4>`
return yo`<form class="${css.permissionForm}" data-id="pluginManagerSettingsPermissionForm">
${title}
<hr/>
${byToPlugin}
</form>`
}
render () {
return yo`
<footer class="bg-light ${css.permissions} remix-bg-opacity">
<button onclick="${() => this.openDialog()}" class="btn btn-primary settings-button" data-id="pluginManagerPermissionsButton">Permissions</button>
</footer>`
}
}

@ -1,156 +0,0 @@
import { AbstractPanel } from './panel'
import * as packageJson from '../../../../../package.json'
const csjs = require('csjs-inject')
const yo = require('yo-yo')
const css = csjs`
.panel {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
flex: auto;
}
.swapitTitle {
margin: 0;
text-transform: uppercase;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.swapitTitle i{
padding-left: 6px;
font-size: 14px;
}
.swapitHeader {
display: flex;
align-items: center;
padding: 16px 24px 15px;
justify-content: space-between;
}
.icons i {
height: 80%;
cursor: pointer;
}
.pluginsContainer {
height: 100%;
overflow-y: auto;
}
.titleInfo {
padding-left: 10px;
}
.versionBadge {
background-color: var(--light);
padding: 0 7px;
font-weight: bolder;
margin-left: 5px;
text-transform: lowercase;
cursor: default;
}
`
const sidePanel = {
name: 'sidePanel',
displayName: 'Side Panel',
description: '',
version: packageJson.version,
methods: ['addView', 'removeView']
}
// TODO merge with vertical-icons.js
export class SidePanel extends AbstractPanel {
constructor (appManager, verticalIcons) {
super(sidePanel)
this.appManager = appManager
this.header = yo`<header></header>`
this.renderHeader()
this.verticalIcons = verticalIcons
// Toggle content
verticalIcons.events.on('toggleContent', (name) => {
if (!this.contents[name]) return
if (this.active === name) {
// TODO: Only keep `this.emit` (issue#2210)
this.emit('toggle', name)
this.events.emit('toggle', name)
return
}
this.showContent(name)
// TODO: Only keep `this.emit` (issue#2210)
this.emit('showing', name)
this.events.emit('showing', name)
})
// Force opening
verticalIcons.events.on('showContent', (name) => {
if (!this.contents[name]) return
this.showContent(name)
// TODO: Only keep `this.emit` (issue#2210)
this.emit('showing', name)
this.events.emit('showing', name)
})
}
focus (name) {
this.emit('focusChanged', name)
super.focus(name)
}
removeView (profile) {
super.removeView(profile)
this.emit('pluginDisabled', profile.name)
this.verticalIcons.unlinkContent(profile)
}
addView (profile, view) {
super.addView(profile, view)
this.verticalIcons.linkContent(profile)
}
/**
* Display content and update the header
* @param {String} name The name of the plugin to display
*/
async showContent (name) {
super.showContent(name)
this.renderHeader()
this.emit('focusChanged', name)
}
/** The header of the side panel */
async renderHeader () {
let name = ' - '
let docLink = ''
let versionWarning
if (this.active) {
const profile = await this.appManager.getProfile(this.active)
name = profile.displayName ? profile.displayName : profile.name
docLink = profile.documentation ? yo`<a href="${profile.documentation}" class="${css.titleInfo}" title="link to documentation" target="_blank"><i aria-hidden="true" class="fas fa-book"></i></a>` : ''
if (profile.version && profile.version.match(/\b(\w*alpha\w*)\b/g)) {
versionWarning = yo`<small title="Version Alpha" class="badge-light ${css.versionBadge}">alpha</small>`
}
// Beta
if (profile.version && profile.version.match(/\b(\w*beta\w*)\b/g)) {
versionWarning = yo`<small title="Version Beta" class="badge-light ${css.versionBadge}">beta</small>`
}
}
const header = yo`
<header class="${css.swapitHeader}">
<h6 class="${css.swapitTitle}" data-id="sidePanelSwapitTitle">${name}</h6>
${docLink}
${versionWarning}
</header>
`
yo.update(this.header, header)
}
render () {
return yo`
<section class="${css.panel} plugin-manager">
${this.header}
<div class="${css.pluginsContainer}">
${this.view}
</div>
</section>`
}
}

@ -0,0 +1,88 @@
// eslint-disable-next-line no-use-before-define
import React from 'react'
import ReactDOM from 'react-dom'
import { AbstractPanel } from './panel'
import { RemixPluginPanel } from '@remix-ui/panel'
import packageJson from '../../../../../package.json'
import RemixUIPanelHeader from 'libs/remix-ui/panel/src/lib/plugins/panel-header'
// const csjs = require('csjs-inject')
const sidePanel = {
name: 'sidePanel',
displayName: 'Side Panel',
description: '',
version: packageJson.version,
methods: ['addView', 'removeView']
}
export class SidePanel extends AbstractPanel {
sideelement: any
constructor() {
super(sidePanel)
this.sideelement = document.createElement('section')
this.sideelement.setAttribute('class', 'panel plugin-manager')
}
onActivation() {
this.renderComponent()
// Toggle content
this.on('menuicons', 'toggleContent', (name) => {
if (!this.plugins[name]) return
if (this.plugins[name].active) {
// TODO: Only keep `this.emit` (issue#2210)
this.emit('toggle', name)
this.events.emit('toggle', name)
return
}
this.showContent(name)
// TODO: Only keep `this.emit` (issue#2210)
this.emit('showing', name)
this.events.emit('showing', name)
})
// Force opening
this.on('menuicons', 'showContent', (name) => {
if (!this.plugins[name]) return
this.showContent(name)
// TODO: Only keep `this.emit` (issue#2210)
this.emit('showing', name)
this.events.emit('showing', name)
})
}
focus(name) {
this.emit('focusChanged', name)
super.focus(name)
}
removeView(profile) {
if (this.plugins[profile.name].active) this.call('menuicons', 'select', 'filePanel')
super.removeView(profile)
this.emit('pluginDisabled', profile.name)
this.call('menuicons', 'unlinkContent', profile)
this.renderComponent()
}
addView(profile, view) {
super.addView(profile, view)
this.call('menuicons', 'linkContent', profile)
this.renderComponent()
}
/**
* Display content and update the header
* @param {String} name The name of the plugin to display
*/
async showContent(name) {
super.showContent(name)
this.emit('focusChanged', name)
this.renderComponent()
}
render() {
return this.sideelement
}
renderComponent() {
ReactDOM.render(<RemixPluginPanel header={<RemixUIPanelHeader plugins={this.plugins}></RemixUIPanelHeader>} plugins={this.plugins} />, this.sideelement)
}
}

@ -1,355 +0,0 @@
import * as packageJson from '../../../../../package.json'
import { basicLogo } from '../ui/svgLogo'
var yo = require('yo-yo')
var csjs = require('csjs-inject')
var helper = require('../../lib/helper')
const globalRegistry = require('../../global/registry')
const contextMenu = require('../ui/contextMenu')
const { Plugin } = require('@remixproject/engine')
const EventEmitter = require('events')
let VERTICALMENU_HANDLE
const profile = {
name: 'menuicons',
displayName: 'Vertical Icons',
description: '',
version: packageJson.version,
methods: ['select']
}
// TODO merge with side-panel.js. VerticalIcons should not be a plugin
export class VerticalIcons extends Plugin {
constructor (appManager) {
super(profile)
this.events = new EventEmitter()
this.appManager = appManager
this.icons = {}
this.iconKind = {}
this.iconStatus = {}
const themeModule = globalRegistry.get('themeModule').api
themeModule.events.on('themeChanged', (theme) => {
this.onThemeChanged(theme.quality)
})
}
linkContent (profile) {
if (!profile.icon) return
this.addIcon(profile)
this.listenOnStatus(profile)
}
unlinkContent (profile) {
this.removeIcon(profile)
}
listenOnStatus (profile) {
// the list of supported keys. 'none' will remove the status
const keys = ['edited', 'succeed', 'none', 'loading', 'failed']
const types = ['error', 'warning', 'success', 'info', '']
const fn = (status) => {
if (!types.includes(status.type) && status.type) throw new Error(`type should be ${keys.join()}`)
if (status.key === undefined) throw new Error('status key should be defined')
if (typeof status.key === 'string' && (!keys.includes(status.key))) {
throw new Error('key should contain either number or ' + keys.join())
}
this.setIconStatus(profile.name, status)
}
this.iconStatus[profile.name] = fn
this.on(profile.name, 'statusChanged', this.iconStatus[profile.name])
}
/**
* Add an icon to the map
* @param {ModuleProfile} profile The profile of the module
*/
addIcon ({ kind, name, icon, displayName, tooltip, documentation }) {
let title = (tooltip || displayName || name)
title = title.replace(/^\w/, c => c.toUpperCase())
this.icons[name] = yo`
<div
class="${css.icon} m-2"
onclick="${() => { this.toggle(name) }}"
plugin="${name}"
title="${title}"
oncontextmenu="${(e) => this.itemContextMenu(e, name, documentation)}"
data-id="verticalIconsKind${name}"
id="verticalIconsKind${name}"
>
<img class="image" src="${icon}" alt="${name}" />
</div>`
this.iconKind[kind || 'none'].appendChild(this.icons[name])
}
/**
* resolve a classes list for @arg key
* @param {Object} key
* @param {Object} type
*/
resolveClasses (key, type) {
let classes = css.status
switch (key) {
case 'succeed':
classes += ' fas fa-check-circle text-' + type + ' ' + css.statusCheck
break
case 'edited':
classes += ' fas fa-sync text-' + type + ' ' + css.statusCheck
break
case 'loading':
classes += ' fas fa-spinner text-' + type + ' ' + css.statusCheck
break
case 'failed':
classes += ' fas fa-exclamation-triangle text-' + type + ' ' + css.statusCheck
break
default: {
classes += ' badge badge-pill badge-' + type
}
}
return classes
}
/**
* Set a new status for the @arg name
* @param {String} name
* @param {Object} status
*/
setIconStatus (name, status) {
const el = this.icons[name]
if (!el) return
const statusEl = el.querySelector('i')
if (statusEl) {
el.removeChild(statusEl)
}
if (status.key === 'none') return // remove status
let text = ''
let key = ''
if (typeof status.key === 'number') {
key = status.key.toString()
text = key
} else key = helper.checkSpecialChars(status.key) ? '' : status.key
let type = ''
if (status.type === 'error') {
type = 'danger' // to use with bootstrap
} else type = helper.checkSpecialChars(status.type) ? '' : status.type
const title = helper.checkSpecialChars(status.title) ? '' : status.title
el.appendChild(yo`<i
title="${title}"
class="${this.resolveClasses(key, type)}"
aria-hidden="true"
>
${text}
</i>`)
el.classList.add(`${css.icon}`)
}
/**
* Remove an icon from the map
* @param {ModuleProfile} profile The profile of the module
*/
removeIcon ({ kind, name }) {
if (this.icons[name]) this.iconKind[kind || 'none'].removeChild(this.icons[name])
}
/**
* Remove active for the current activated icons
*/
removeActive () {
// reset filters
const images = this.view.querySelectorAll('.image')
images.forEach(function (im) {
im.style.setProperty('filter', 'invert(0.5)')
})
// remove active
const currentActive = this.view.querySelector('.active')
if (currentActive) {
currentActive.classList.remove('active')
}
}
/**
* Add active for the new activated icon
* @param {string} name Name of profile of the module to activate
*/
addActive (name) {
if (name === 'home') return
const themeType = globalRegistry.get('themeModule').api.currentTheme().quality
const invert = themeType === 'dark' ? 1 : 0
const brightness = themeType === 'dark' ? '150' : '0' // should be >100 for icons with color
const nextActive = this.view.querySelector(`[plugin="${name}"]`)
if (nextActive) {
const image = nextActive.querySelector('.image')
nextActive.classList.add('active')
image.style.setProperty('filter', `invert(${invert}) grayscale(1) brightness(${brightness}%)`)
}
}
/**
* Set an icon as active
* @param {string} name Name of profile of the module to activate
*/
select (name) {
this.updateActivations(name)
// TODO: Only keep `this.emit` (issue#2210)
this.emit('showContent', name)
this.events.emit('showContent', name)
}
/**
* Toggles the side panel for plugin
* @param {string} name Name of profile of the module to activate
*/
toggle (name) {
this.updateActivations(name)
// TODO: Only keep `this.emit` (issue#2210)
this.emit('toggleContent', name)
this.events.emit('toggleContent', name)
}
updateActivations (name) {
this.removeActive()
this.addActive(name)
}
onThemeChanged (themeType) {
const invert = themeType === 'dark' ? 1 : 0
const active = this.view.querySelector('.active')
if (active) {
const image = active.querySelector('.image')
image.style.setProperty('filter', `invert(${invert})`)
}
}
async itemContextMenu (e, name, documentation) {
const actions = {}
if (await this.appManager.canDeactivatePlugin(profile, { name })) {
actions.Deactivate = () => {
// this.call('manager', 'deactivatePlugin', name)
this.appManager.deactivatePlugin(name)
if (e.target.parentElement.classList.contains('active')) {
this.select('filePanel')
}
}
}
const links = {}
if (documentation) {
links.Documentation = documentation
}
if (Object.keys(actions).length || Object.keys(links).length) {
VERTICALMENU_HANDLE && VERTICALMENU_HANDLE.hide(null, true)
VERTICALMENU_HANDLE = contextMenu(e, actions, links)
}
e.preventDefault()
e.stopPropagation()
}
render () {
const home = yo`
<div
class="m-1 mt-2 ${css.homeIcon}"
onclick="${async () => {
await this.appManager.activatePlugin('home')
this.call('tabs', 'focus', 'home')
}}"
plugin="home" title="Home"
data-id="verticalIconsHomeIcon"
id="verticalIconsHomeIcon"
>
${basicLogo()}
</div>
`
this.iconKind.fileexplorer = yo`<div id='fileExplorerIcons' data-id="verticalIconsFileExplorerIcons"></div>`
this.iconKind.compiler = yo`<div id='compileIcons'></div>`
this.iconKind.udapp = yo`<div id='runIcons'></div>`
this.iconKind.testing = yo`<div id='testingIcons'></div>`
this.iconKind.analysis = yo`<div id='analysisIcons'></div>`
this.iconKind.debugging = yo`<div id='debuggingIcons' data-id="verticalIconsDebuggingIcons"></div>`
this.iconKind.none = yo`<div id='otherIcons'></div>`
this.iconKind.settings = yo`<div id='settingsIcons' data-id="verticalIconsSettingsIcons"></div>`
this.view = yo`
<div class="h-100">
<div class=${css.icons}>
${home}
${this.iconKind.fileexplorer}
${this.iconKind.compiler}
${this.iconKind.udapp}
${this.iconKind.testing}
${this.iconKind.analysis}
${this.iconKind.debugging}
${this.iconKind.none}
${this.iconKind.settings}
</div>
</div>
`
return this.view
}
}
const css = csjs`
.homeIcon {
display: block;
width: 42px;
height: 42px;
margin-bottom: 20px;
cursor: pointer;
}
.homeIcon svg path {
fill: var(--primary);
}
.homeIcon svg polygon {
fill: var(--primary);
}
.icons {
}
.icon {
cursor: pointer;
margin-bottom: 12px;
width: 36px;
height: 36px;
padding: 3px;
position: relative;
border-radius: 8px;
}
.icon img {
width: 28px;
height: 28px;
padding: 4px;
filter: invert(0.5);
}
.image {
}
.icon svg {
width: 28px;
height: 28px;
padding: 4px;
}
.icon[title='Settings'] {
position: absolute;
bottom: 0;
}
.status {
position: absolute;
bottom: 0;
right: 0;
}
.statusCheck {
font-size: 1.2em;
}
.statusWithBG
border-radius: 8px;
background-color: var(--danger);
color: var(--light);
font-size: 12px;
height: 15px;
text-align: center;
font-weight: bold;
padding-left: 5px;
padding-right: 5px;
}
`

@ -0,0 +1,116 @@
// eslint-disable-next-line no-use-before-define
import React from 'react'
import ReactDOM from 'react-dom'
import Registry from '../state/registry'
import packageJson from '../../../../../package.json'
import { Plugin } from '@remixproject/engine'
import { EventEmitter } from 'events'
import { IconRecord, RemixUiVerticalIconsPanel } from '@remix-ui/vertical-icons-panel'
import { Profile } from '@remixproject/plugin-utils'
import { timeStamp } from 'console'
const profile = {
name: 'menuicons',
displayName: 'Vertical Icons',
description: '',
version: packageJson.version,
methods: ['select', 'unlinkContent', 'linkContent'],
events: ['toggleContent', 'showContent']
}
export class VerticalIcons extends Plugin {
events: EventEmitter
htmlElement: HTMLDivElement
icons: Record<string, IconRecord> = {}
constructor () {
super(profile)
this.events = new EventEmitter()
this.htmlElement = document.createElement('div')
this.htmlElement.setAttribute('id', 'icon-panel')
}
renderComponent () {
const fixedOrder = ['filePanel', 'solidity','udapp', 'debugger', 'solidityStaticAnalysis', 'solidityUnitTesting', 'pluginManager']
const divived = Object.values(this.icons).map((value) => { return {
...value,
isRequired: fixedOrder.indexOf(value.profile.name) > -1
}}).sort((a,b) => {
return a.timestamp - b.timestamp
})
const required = divived.filter((value) => value.isRequired).sort((a,b) => {
return fixedOrder.indexOf(a.profile.name) - fixedOrder.indexOf(b.profile.name)
})
const sorted: IconRecord[] = [
...required,
...divived.filter((value) => { return !value.isRequired })
]
ReactDOM.render(
<RemixUiVerticalIconsPanel
verticalIconsPlugin={this}
icons={sorted}
/>,
this.htmlElement)
}
onActivation () {
this.renderComponent()
this.on('sidePanel', 'focusChanged', (name: string) => {
Object.keys(this.icons).map((o) => {
this.icons[o].active = false
})
this.icons[name].active = true
this.renderComponent()
})
}
async linkContent (profile: Profile) {
if (!profile.icon) return
if (!profile.kind) profile.kind = 'none'
this.icons[profile.name] = {
profile: profile,
active: false,
canbeDeactivated: await this.call('manager', 'canDeactivate', this.profile, profile),
timestamp: Date.now()
}
this.renderComponent()
}
unlinkContent (profile: Profile) {
delete this.icons[profile.name]
this.renderComponent()
}
async activateHome() {
await this.call('manager', 'activatePlugin', 'home')
await this.call('tabs', 'focus', 'home')
}
/**
* Set an icon as active
* @param {string} name Name of profile of the module to activate
*/
select (name: string) {
// TODO: Only keep `this.emit` (issue#2210)
console.log(name, this)
this.emit('showContent', name)
this.events.emit('showContent', name)
}
/**
* Toggles the side panel for plugin
* @param {string} name Name of profile of the module to activate
*/
toggle (name: string) {
// TODO: Only keep `this.emit` (issue#2210)
this.emit('toggleContent', name)
this.events.emit('toggleContent', name)
}
render () {
return this.htmlElement
}
}

@ -1,194 +0,0 @@
'use strict'
import { sourceMappingDecoder } from '@remix-project/remix-debug'
const yo = require('yo-yo')
const globalRegistry = require('../../global/registry')
const css = require('./styles/contextView-styles')
/*
Display information about the current focused code:
- if it's a reference, display information about the declaration
- jump to the declaration
- number of references
- rename declaration/references
*/
class ContextView {
constructor (opts, localRegistry) {
this._components = {}
this._components.registry = localRegistry || globalRegistry
this.contextualListener = opts.contextualListener
this.editor = opts.editor
this._deps = {
compilersArtefacts: this._components.registry.get('compilersartefacts').api,
offsetToLineColumnConverter: this._components.registry.get('offsettolinecolumnconverter').api,
config: this._components.registry.get('config').api,
fileManager: this._components.registry.get('filemanager').api
}
this._view = null
this._nodes = null
this._current = null
this.sourceMappingDecoder = sourceMappingDecoder
this.previousElement = null
this.contextualListener.event.register('contextChanged', nodes => {
this.show()
this._nodes = nodes
this.update()
})
this.contextualListener.event.register('stopHighlighting', () => {
})
}
render () {
const view = yo`
<div class="${css.contextview} ${css.contextviewcontainer} bg-light text-dark border-0">
<div class=${css.container}>
${this._renderTarget()}
</div>
</div>`
if (!this._view) {
this._view = view
}
return view
}
hide () {
if (this._view) {
this._view.style.display = 'none'
}
}
show () {
if (this._view) {
this._view.style.display = 'block'
}
}
update () {
if (this._view) {
yo.update(this._view, this.render())
}
}
_renderTarget () {
let last
const previous = this._current
if (this._nodes && this._nodes.length) {
last = this._nodes[this._nodes.length - 1]
if (isDefinition(last)) {
this._current = last
} else {
const target = this.contextualListener.declarationOf(last)
if (target) {
this._current = target
} else {
this._current = null
}
}
}
if (!this._current || !previous || previous.id !== this._current.id || (this.previousElement && !this.previousElement.children.length)) {
this.previousElement = this._render(this._current, last)
}
return this.previousElement
}
_jumpToInternal (position) {
const jumpToLine = (lineColumn) => {
if (lineColumn.start && lineColumn.start.line && lineColumn.start.column) {
this.editor.gotoLine(lineColumn.start.line, lineColumn.end.column + 1)
}
}
const lastCompilationResult = this._deps.compilersArtefacts.__last
if (lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0 && lastCompilationResult.data) {
const lineColumn = this._deps.offsetToLineColumnConverter.offsetToLineColumn(
position,
position.file,
lastCompilationResult.getSourceCode().sources,
lastCompilationResult.getAsts())
const filename = lastCompilationResult.getSourceName(position.file)
// TODO: refactor with rendererAPI.errorClick
if (filename !== this._deps.config.get('currentFile')) {
const provider = this._deps.fileManager.fileProviderOf(filename)
if (provider) {
provider.exists(filename).then(exist => {
this._deps.fileManager.open(filename)
jumpToLine(lineColumn)
}).catch(error => {
if (error) return console.log(error)
})
}
} else {
jumpToLine(lineColumn)
}
}
}
_render (node, nodeAtCursorPosition) {
if (!node) return yo`<div></div>`
let references = this.contextualListener.referencesOf(node)
const type = node.typeDescriptions && node.typeDescriptions.typeString ? node.typeDescriptions.typeString : node.nodeType
references = `${references ? references.length : '0'} reference(s)`
let ref = 0
const nodes = this.contextualListener.getActiveHighlights()
for (const k in nodes) {
if (nodeAtCursorPosition.id === nodes[k].nodeId) {
ref = k
break
}
}
// JUMP BETWEEN REFERENCES
const jump = (e) => {
e.target.dataset.action === 'next' ? ref++ : ref--
if (ref < 0) ref = nodes.length - 1
if (ref >= nodes.length) ref = 0
this._jumpToInternal(nodes[ref].position)
}
const jumpTo = () => {
if (node && node.src) {
const position = this.sourceMappingDecoder.decode(node.src)
if (position) {
this._jumpToInternal(position)
}
}
}
const showGasEstimation = () => {
if (node.nodeType === 'FunctionDefinition') {
const result = this.contextualListener.gasEstimation(node)
const executionCost = ' Execution cost: ' + result.executionCost + ' gas'
const codeDepositCost = 'Code deposit cost: ' + result.codeDepositCost + ' gas'
const estimatedGas = result.codeDepositCost ? `${codeDepositCost}, ${executionCost}` : `${executionCost}`
return yo`
<div class=${css.gasEstimation}>
<i class="fas fa-gas-pump ${css.gasStationIcon}" title='Gas estimation'></i>
<span>${estimatedGas}</span>
</div>
`
}
}
return yo`
<div class=${css.line}>${showGasEstimation()}
<div title=${type} class=${css.type}>${type}</div>
<div title=${node.name} class=${css.name}>${node.name}</div>
<i class="fas fa-share ${css.jump}" aria-hidden="true" onclick=${jumpTo}></i>
<span class=${css.referencesnb}>${references}</span>
<i data-action='previous' class="fas fa-chevron-up ${css.jump}" aria-hidden="true" onclick=${jump}></i>
<i data-action='next' class="fas fa-chevron-down ${css.jump}" aria-hidden="true" onclick=${jump}></i>
</div>
`
}
}
function isDefinition (node) {
return node.nodeType === 'ContractDefinition' ||
node.nodeType === 'FunctionDefinition' ||
node.nodeType === 'ModifierDefinition' ||
node.nodeType === 'VariableDeclaration' ||
node.nodeType === 'StructDefinition' ||
node.nodeType === 'EventDefinition'
}
module.exports = ContextView

@ -12,7 +12,7 @@ const profile = {
name: 'editor',
description: 'service - editor',
version: packageJson.version,
methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addAnnotation', 'gotoLine']
methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addAnnotation', 'gotoLine', 'getCursorPosition']
}
class Editor extends Plugin {
@ -46,7 +46,8 @@ class Editor extends Plugin {
txt: 'text',
json: 'json',
abi: 'json',
rs: 'rust'
rs: 'rust',
cairo: 'cairo'
}
this.activated = false
@ -74,7 +75,8 @@ class Editor extends Plugin {
this._onChange(this.currentFile)
}
}
this.el.gotoLine = (line) => this.gotoLine(line, 0)
this.el.gotoLine = (line, column) => this.gotoLine(line, column || 0)
this.el.getCursorPosition = () => this.getCursorPosition()
return this.el
}
@ -436,7 +438,7 @@ class Editor extends Plugin {
if (!filePath) return
filePath = await this.call('fileManager', 'getPathFromUrl', filePath)
filePath = filePath.file
if (!this.sessions[filePath]) throw new Error('file not found' + filePath)
if (!this.sessions[filePath]) return
const path = filePath || this.currentFile
const { from } = this.currentRequest

@ -313,7 +313,7 @@ const deployWithEthers = `// Right click on the script name and hit "Run" to exe
const readme = `REMIX EXAMPLE PROJECT
Remix example project is present when Remix loads very first time or there are no files existing in the File Explorer.
Remix example project is present when Remix loads for the very first time or there are no files existing in the File Explorer.
It contains 3 directories:
1. 'contracts': Holds three contracts with different complexity level, denoted with number prefix in file name.

@ -1,12 +1,10 @@
'use strict'
import yo from 'yo-yo'
import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../package.json'
const EventEmitter = require('events')
const globalRegistry = require('../../global/registry')
const toaster = require('../ui/tooltip')
const modalDialogCustom = require('../ui/modal-dialog-custom')
import Registry from '../state/registry'
import { EventEmitter } from 'events'
import { RemixAppManager } from '../../../../../libs/remix-ui/plugin-manager/src/types'
import { fileChangedToastMsg } from '@remix-ui/helper'
const helper = require('../../lib/helper.js')
/*
@ -21,7 +19,7 @@ const profile = {
icon: 'assets/img/fileManager.webp',
permission: true,
version: packageJson.version,
methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile'],
methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile', 'setBatchFiles'],
kind: 'file-system'
}
const errorMsg = {
@ -36,6 +34,18 @@ const createError = (err) => {
}
class FileManager extends Plugin {
mode: string
openedFiles: any
events: EventEmitter
editor: any
_components: any
appManager: RemixAppManager
_deps: any
getCurrentFile: () => any
getFile: (path: any) => Promise<unknown>
getFolder: (path: any) => Promise<unknown>
setFile: (path: any, data: any) => Promise<unknown>
switchFile: (path: any) => Promise<void>
constructor (editor, appManager) {
super(profile)
this.mode = 'browser'
@ -43,7 +53,7 @@ class FileManager extends Plugin {
this.events = new EventEmitter()
this.editor = editor
this._components = {}
this._components.registry = globalRegistry
this._components.registry = Registry.getInstance()
this.appManager = appManager
this.init()
}
@ -69,7 +79,7 @@ class FileManager extends Plugin {
* @param {string} path path of the file/directory
* @param {string} message message to display if path doesn't exist.
*/
async _handleExists (path, message) {
async _handleExists (path: string, message?:string) {
const exists = await this.exists(path)
if (!exists) {
@ -95,7 +105,7 @@ class FileManager extends Plugin {
* @param {string} path path of the file/directory
* @param {string} message message to display if path is not a directory.
*/
async _handleIsDir (path, message) {
async _handleIsDir (path: string, message?: string) {
const isDir = await this.isDirectory(path)
if (!isDir) {
@ -303,13 +313,19 @@ class FileManager extends Plugin {
if (isFile) {
if (newPathExists) {
modalDialogCustom.alert('File already exists.')
this.call('notification', 'alert', {
id: 'fileManagerAlert',
message: 'File already exists'
})
return
}
return provider.rename(oldPath, newPath, false)
} else {
if (newPathExists) {
modalDialogCustom.alert('Folder already exists.')
this.call('notification', 'alert', {
id: 'fileManagerAlert',
message: 'Directory already exists'
})
return
}
return provider.rename(oldPath, newPath, true)
@ -451,7 +467,7 @@ class FileManager extends Plugin {
}
currentFile () {
return this._deps.config.get('currentFile')
return this.editor.current()
}
async closeAllFiles () {
@ -507,17 +523,7 @@ class FileManager extends Plugin {
const required = this.appManager.isRequired(this.currentRequest.from)
if (canCall && !required) {
// inform the user about modification after permission is granted and even if permission was saved before
toaster(yo`
<div>
<i class="fas fa-exclamation-triangle text-danger mr-1"></i>
<span>
${this.currentRequest.from}
<span class="font-weight-bold text-warning">
is modifying
</span>${path}
</span>
</div>
`, '', { time: 3000 })
this.call('notification','toast', fileChangedToastMsg(this.currentRequest.from, path))
}
}
return await this._setFileInternal(path, content)
@ -609,7 +615,7 @@ class FileManager extends Plugin {
this.events.emit('noFileSelected')
}
async openFile (file) {
async openFile (file?: string) {
if (!file) {
this.emit('noFileSelected')
this.events.emit('noFileSelected')
@ -631,12 +637,20 @@ class FileManager extends Plugin {
if (provider.isReadOnly(file)) {
this.editor.openReadOnly(file, content)
} else {
this.editor.open(file, content)
if (provider.isReadOnly(file)) {
this.editor.openReadOnly(file, content)
} else {
this.editor.open(file, content)
}
// TODO: Only keep `this.emit` (issue#2210)
this.emit('currentFileChanged', file)
this.events.emit('currentFileChanged', file)
resolve(true)
}
// TODO: Only keep `this.emit` (issue#2210)
this.emit('currentFileChanged', file)
this.events.emit('currentFileChanged', file)
resolve()
resolve(true)
}
})
})
@ -694,7 +708,7 @@ class FileManager extends Plugin {
dirPaths.push(item)
resolve(dirPaths)
}
return new Promise((resolve, reject) => { resolve() })
return new Promise((resolve, reject) => { resolve(true) })
})
Promise.all(promises).then(() => { resolve(dirPaths) })
})
@ -738,32 +752,6 @@ class FileManager extends Plugin {
}
async setBatchFiles (filesSet, fileProvider, override, callback) {
const self = this
if (!fileProvider) fileProvider = 'workspace'
if (override === undefined) override = false
for (const file of Object.keys(filesSet)) {
if (override) {
await self._deps.filesProviders[fileProvider].set(file, filesSet[file].content, (e) => {
if (e) callback(e.message || e)
})
await self.syncEditor(fileProvider + file)
} else {
try {
const name = await helper.createNonClashingNameAsync(file, self)
if (helper.checkSpecialChars(name)) {
modalDialogCustom.alert('Special characters are not allowed')
} else {
await self._deps.filesProviders[fileProvider].set(name, filesSet[file].content)
await self.syncEditor(fileProvider + name)
}
} catch (error) {
modalDialogCustom.alert('Unexpected error loading the file ' + error)
return callback(error.message || error)
}
callback()
}
}
}
currentWorkspace () {

@ -2,8 +2,6 @@
import { CompilerImports } from '@remix-project/core-plugin'
const EventManager = require('events')
const modalDialogCustom = require('../ui/modal-dialog-custom')
const tooltip = require('../ui/tooltip')
const remixLib = require('@remix-project/remix-lib')
const Storage = remixLib.Storage
@ -49,7 +47,7 @@ class FileProvider {
return this.externalFolders.includes(path)
}
discardChanges (path) {
discardChanges (path, toastCb, modalCb) {
this.remove(path)
const compilerImport = new CompilerImports()
this.providerExternalsStorage.keys().map(value => {
@ -57,10 +55,10 @@ class FileProvider {
compilerImport.import(
this.getNormalizedName(value),
true,
(loadingMsg) => { tooltip(loadingMsg) },
async (error, content, cleanUrl, type, url) => {
(loadingMsg) => { toastCb(loadingMsg) },
(error, content, cleanUrl, type, url) => {
if (error) {
modalDialogCustom.alert(error)
modalCb(error)
} else {
await this.addExternal(type + '/' + cleanUrl, content, url)
}

@ -4,11 +4,12 @@ import * as packageJson from '../../../../../package.json'
import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
import { FileSystemProvider } from '@remix-ui/workspace' // eslint-disable-line
const { RemixdHandle } = require('../files/remixd-handle.js')
import Registry from '../state/registry'
import { RemixdHandle } from '../plugins/remixd-handle'
const { GitHandle } = require('../files/git-handle.js')
const { HardhatHandle } = require('../files/hardhat-handle.js')
const { SlitherHandle } = require('../files/slither-handle.js')
const globalRegistry = require('../../global/registry')
/*
Overview of APIs:
* fileManager: @args fileProviders (browser, shared-folder, swarm, github, etc ...) & config & editor
@ -41,7 +42,7 @@ const profile = {
module.exports = class Filepanel extends ViewPlugin {
constructor (appManager) {
super(profile)
this.registry = globalRegistry
this.registry = Registry.getInstance()
this.fileProviders = this.registry.get('fileproviders').api
this.fileManager = this.registry.get('filemanager').api

@ -0,0 +1,95 @@
import { Plugin } from '@remixproject/engine'
import { Profile } from '@remixproject/plugin-utils'
import { EventEmitter } from 'events'
import QueryParams from '../../lib/query-params'
const profile: Profile = {
name: 'layout',
description: 'layout',
methods: ['minimize']
}
interface panelState {
active: boolean
plugin: Plugin
minimized: boolean
}
interface panels {
tabs: panelState
editor: panelState
main: panelState
terminal: panelState
}
export class Layout extends Plugin {
event: any
panels: panels
constructor () {
super(profile)
this.event = new EventEmitter()
}
async onActivation (): Promise<void> {
this.on('fileManager', 'currentFileChanged', () => {
this.panels.editor.active = true
this.panels.main.active = false
this.event.emit('change', null)
})
this.on('tabs', 'openFile', () => {
this.panels.editor.active = true
this.panels.main.active = false
this.event.emit('change', null)
})
this.on('tabs', 'switchApp', (name: string) => {
this.call('mainPanel', 'showContent', name)
this.panels.editor.active = false
this.panels.main.active = true
this.event.emit('change', null)
})
this.on('tabs', 'closeApp', (name: string) => {
this.panels.editor.active = true
this.panels.main.active = false
this.event.emit('change', null)
})
this.on('tabs', 'tabCountChanged', async count => {
if (!count) await this.call('manager', 'activatePlugin', 'home')
})
this.on('manager', 'activate', (profile: Profile) => {
switch (profile.name) {
case 'filePanel':
this.call('menuicons', 'select', 'filePanel')
break
}
})
document.addEventListener('keypress', e => {
if (e.shiftKey && e.ctrlKey) {
if (e.code === 'KeyF') {
// Ctrl+Shift+F
this.call('menuicons', 'select', 'filePanel')
} else if (e.code === 'KeyA') {
// Ctrl+Shift+A
this.call('menuicons', 'select', 'pluginManager')
} else if (e.code === 'KeyS') {
// Ctrl+Shift+S
this.call('menuicons', 'select', 'settings')
}
e.preventDefault()
}
})
const queryParams = new QueryParams()
const params = queryParams.get()
if (params.minimizeterminal || params.embed) {
this.panels.terminal.minimized = true
this.event.emit('change', this.panels)
this.emit('change', this.panels)
}
if (params.minimizesidepanel || params.embed) {
this.event.emit('minimizesidepanel')
}
}
minimize (name: string, minimized:boolean): void {
this.panels[name].minimized = minimized
this.event.emit('change', null)
}
}

@ -1,211 +0,0 @@
var yo = require('yo-yo')
var EventManager = require('../../lib/events')
var globalRegistry = require('../../global/registry')
var { TabProxy } = require('./tab-proxy.js')
var ContextView = require('../editor/contextView')
var csjs = require('csjs-inject')
var css = csjs`
.mainview {
display : flex;
flex-direction : column;
height : 100%;
width : 100%;
}
`
// @todo(#650) Extract this into two classes: MainPanel (TabsProxy + Iframe/Editor) & BottomPanel (Terminal)
export class MainView {
constructor (contextualListener, editor, mainPanel, fileManager, appManager, terminal) {
var self = this
self.event = new EventManager()
self._view = {}
self._components = {}
self._components.registry = globalRegistry
self.editor = editor
self.fileManager = fileManager
self.mainPanel = mainPanel
self.txListener = globalRegistry.get('txlistener').api
self._components.terminal = terminal
self._components.contextualListener = contextualListener
this.appManager = appManager
this.init()
}
async showApp (name) {
await this.fileManager.unselectCurrentFile()
this.mainPanel.showContent(name)
this._view.editor.style.display = 'none'
this._components.contextView.hide()
this._view.mainPanel.style.display = 'block'
}
getAppPanel () {
return this.mainPanel
}
init () {
var self = this
self._deps = {
config: self._components.registry.get('config').api,
fileManager: self._components.registry.get('filemanager').api
}
self.tabProxy = new TabProxy(self.fileManager, self.editor)
/*
We listen here on event from the tab component to display / hide the editor and mainpanel
depending on the content that should be displayed
*/
self.fileManager.events.on('currentFileChanged', (file) => {
// we check upstream for "fileChanged"
self._view.editor.style.display = 'block'
self._view.mainPanel.style.display = 'none'
self._components.contextView.show()
})
self.tabProxy.event.on('openFile', (file) => {
self._view.editor.style.display = 'block'
self._view.mainPanel.style.display = 'none'
self._components.contextView.show()
})
self.tabProxy.event.on('closeFile', (file) => {
})
self.tabProxy.event.on('switchApp', self.showApp.bind(self))
self.tabProxy.event.on('closeApp', (name) => {
self._view.editor.style.display = 'block'
self._components.contextView.show()
self._view.mainPanel.style.display = 'none'
})
self.tabProxy.event.on('tabCountChanged', (count) => {
if (!count) this.editor.displayEmptyReadOnlySession()
})
self.data = {
_layout: {
top: {
offset: self._terminalTopOffset(),
show: true
}
}
}
const contextView = new ContextView({ contextualListener: self._components.contextualListener, editor: self.editor })
self._components.contextView = contextView
self._components.terminal.event.register('resize', delta => self._adjustLayout('top', delta))
if (self.txListener) {
self._components.terminal.event.register('listenOnNetWork', (listenOnNetWork) => {
self.txListener.setListenOnNetwork(listenOnNetWork)
})
}
}
_terminalTopOffset () {
return this._deps.config.get('terminal-top-offset') || 150
}
_adjustLayout (direction, delta) {
var limitUp = 0
var limitDown = 32
var containerHeight = window.innerHeight - limitUp // - menu bar containerHeight
var self = this
var layout = self.data._layout[direction]
if (layout) {
if (delta === undefined) {
layout.show = !layout.show
if (layout.show) delta = layout.offset
else delta = 0
} else {
layout.show = true
self._deps.config.set(`terminal-${direction}-offset`, delta)
layout.offset = delta
}
}
var tmp = delta - limitDown
delta = tmp > 0 ? tmp : 0
if (direction === 'top') {
var mainPanelHeight = containerHeight - delta
mainPanelHeight = mainPanelHeight < 0 ? 0 : mainPanelHeight
self._view.editor.style.height = `${mainPanelHeight}px`
self._view.mainPanel.style.height = `${mainPanelHeight}px`
self._view.terminal.style.height = `${delta}px` // - menu bar height
self.editor.resize((document.querySelector('#editorWrap') || {}).checked)
self._components.terminal.scroll2bottom()
}
}
minimizeTerminal () {
this._adjustLayout('top')
}
showTerminal (offset) {
this._adjustLayout('top', offset || this._terminalTopOffset())
}
getTerminal () {
return this._components.terminal
}
getEditor () {
var self = this
return self.editor
}
refresh () {
var self = this
self._view.tabs.onmouseenter()
}
log (data = {}) {
var self = this
var command = self._components.terminal.commands[data.type]
if (typeof command === 'function') command(data.value)
}
logMessage (msg) {
var self = this
self.log({ type: 'log', value: msg })
}
logHtmlMessage (msg) {
var self = this
self.log({ type: 'html', value: msg })
}
render () {
var self = this
if (self._view.mainview) return self._view.mainview
self._view.editor = self.editor.render()
self._view.editor.style.display = 'none'
self._view.mainPanel = self.mainPanel.render()
self._view.terminal = self._components.terminal.render()
self._view.mainview = yo`
<div class=${css.mainview}>
${self.tabProxy.renderTabsbar()}
${self._view.editor}
${self._view.mainPanel}
${self._components.contextView.render()}
${self._view.terminal}
</div>
`
// INIT
self._adjustLayout('top', self.data._layout.top.offset)
document.addEventListener('keydown', (e) => {
if (e.altKey && e.keyCode === 84) self.tabProxy.switchNextTab() // alt + t
})
return self._view.mainview
}
registerCommand (name, command, opts) {
var self = this
return self._components.terminal.registerCommand(name, command, opts)
}
updateTerminalFilter (filter) {
this._components.terminal.updateJournal(filter)
}
}

@ -22,6 +22,7 @@ export class TabProxy extends Plugin {
this._view = {}
this._handlers = {}
this.loadedTabs = []
this.el = document.createElement('div')
}
onActivation () {
@ -72,10 +73,12 @@ export class TabProxy extends Plugin {
this.addTab(workspacePath, '', async () => {
await this.fileManager.open(file)
this.event.emit('openFile', file)
this.emit('openFile', file)
},
async () => {
await this.fileManager.closeFile(file)
this.event.emit('closeFile', file)
this.emit('closeFile', file)
})
this.tabsApi.activateTab(workspacePath)
} else {
@ -88,10 +91,12 @@ export class TabProxy extends Plugin {
this.addTab(path, '', async () => {
await this.fileManager.open(file)
this.event.emit('openFile', file)
this.emit('openFile', file)
},
async () => {
await this.fileManager.closeFile(file)
this.event.emit('closeFile', file)
this.emit('closeFile', file)
})
this.tabsApi.activateTab(path)
}
@ -132,9 +137,9 @@ export class TabProxy extends Plugin {
this.addTab(
name,
displayName,
() => this.event.emit('switchApp', name),
() => this.emit('switchApp', name),
() => {
this.event.emit('closeApp', name)
this.emit('closeApp', name)
this.call('manager', 'deactivatePlugin', name)
},
icon
@ -149,7 +154,7 @@ export class TabProxy extends Plugin {
}
focus (name) {
this.event.emit('switchApp', name)
this.emit('switchApp', name)
this.tabsApi.activateTab(name)
}
@ -199,6 +204,7 @@ export class TabProxy extends Plugin {
async () => {
await this.fileManager.closeFile(newName)
this.event.emit('closeFile', newName)
this.emit('closeFile', newName)
})
this.removeTab(oldName)
}
@ -285,7 +291,7 @@ export class TabProxy extends Plugin {
if (this.loadedTabs[index]) {
const name = this.loadedTabs[index].name
if (this._handlers[name]) this._handlers[name].switchTo()
this.event.emit('tabCountChanged', this.loadedTabs.length)
this.emit('tabCountChanged', this.loadedTabs.length)
}
}
@ -293,7 +299,7 @@ export class TabProxy extends Plugin {
if (this.loadedTabs[index]) {
const name = this.loadedTabs[index].name
if (this._handlers[name]) this._handlers[name].close()
this.event.emit('tabCountChanged', this.loadedTabs.length)
this.emit('tabCountChanged', this.loadedTabs.length)
}
}
@ -308,8 +314,6 @@ export class TabProxy extends Plugin {
}
renderTabsbar () {
this.el = document.createElement('div')
this.renderComponent()
return this.el
}
}

@ -4,15 +4,11 @@ import ReactDOM from 'react-dom'
import { RemixUiTerminal } from '@remix-ui/terminal' // eslint-disable-line
import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../package.json'
import Registry from '../state/registry'
const vm = require('vm')
const EventManager = require('../../lib/events')
const CommandInterpreterAPI = require('../../lib/cmdInterpreterAPI')
const AutoCompletePopup = require('../ui/auto-complete-popup')
import { CompilerImports } from '@remix-project/core-plugin' // eslint-disable-line
const globalRegistry = require('../../global/registry')
const GistHandler = require('../../lib/gist-handler')
const KONSOLES = []
@ -21,7 +17,7 @@ function register (api) { KONSOLES.push(api) }
const profile = {
displayName: 'Terminal',
name: 'terminal',
methods: ['log'],
methods: ['log', 'logHtml'],
events: [],
description: ' - ',
version: packageJson.version
@ -31,9 +27,8 @@ class Terminal extends Plugin {
constructor (opts, api) {
super(profile)
this.fileImport = new CompilerImports()
this.gistHandler = new GistHandler()
this.event = new EventManager()
this.globalRegistry = globalRegistry
this.globalRegistry = Registry.getInstance()
this.element = document.createElement('div')
this.element.setAttribute('class', 'panel')
this.element.setAttribute('id', 'terminal-view')
@ -67,8 +62,6 @@ class Terminal extends Plugin {
}
this._view = { el: null, bar: null, input: null, term: null, journal: null, cli: null }
this._components = {}
this._components.cmdInterpreter = new CommandInterpreterAPI(this, null, this.blockchain)
this._components.autoCompletePopup = new AutoCompletePopup(this._opts)
this._commands = {}
this.commands = {}
this._JOURNAL = []

@ -0,0 +1,31 @@
import { Plugin } from '@remixproject/engine'
import QueryParams from '../../lib/query-params'
import Registry from '../state/registry'
const profile = {
name: 'config',
displayName: 'Config',
description: 'Config',
methods: ['getAppParameter', 'setAppParameter']
}
export class ConfigPlugin extends Plugin {
constructor () {
super(profile)
}
getAppParameter (name: string) {
const queryParams = new QueryParams()
const params = queryParams.get()
const config = Registry.getInstance().get('config').api
const param = params[name] ? params[name] : config.get(name)
if (param === 'true') return true
if (param === 'false') return false
return param
}
setAppParameter (name: string, value: any) {
const config = Registry.getInstance().get('config').api
config.set(name, value)
}
}

@ -0,0 +1,44 @@
import { Plugin } from '@remixproject/engine'
import { LibraryProfile, MethodApi, StatusEvents } from '@remixproject/plugin-utils'
import { AppModal } from '@remix-ui/app'
import { AlertModal } from 'libs/remix-ui/app/src/lib/remix-app/interface'
import { dispatchModalInterface } from 'libs/remix-ui/app/src/lib/remix-app/context/context'
interface INotificationApi {
events: StatusEvents,
methods: {
modal: (args: AppModal) => void
alert: (args: AlertModal) => void
toast: (message: string) => void
}
}
const profile:LibraryProfile<INotificationApi> = {
name: 'notification',
displayName: 'Notification',
description: 'Displays notifications',
methods: ['modal', 'alert', 'toast']
}
export class NotificationPlugin extends Plugin implements MethodApi<INotificationApi> {
dispatcher: dispatchModalInterface
constructor () {
super(profile)
}
setDispatcher (dispatcher: dispatchModalInterface) {
this.dispatcher = dispatcher
}
async modal (args: AppModal) {
return this.dispatcher.modal(args)
}
async alert (args: AlertModal) {
return this.dispatcher.alert(args)
}
async toast (message: string | JSX.Element) {
this.dispatcher.toast(message)
}
}

@ -0,0 +1,126 @@
import React from 'react' // eslint-disable-line
import { Plugin } from '@remixproject/engine'
import { AppModal } from 'libs/remix-ui/app/src'
import { PermissionHandlerDialog, PermissionHandlerValue } from 'libs/remix-ui/permission-handler/src'
import { Profile } from '@remixproject/plugin-utils'
const profile = {
name: 'permissionhandler',
displayName: 'permissionhandler',
description: 'permissionhandler',
methods: ['askPermission']
}
export class PermissionHandlerPlugin extends Plugin {
permissions: any
currentVersion: number
constructor() {
super(profile)
this.permissions = this._getFromLocal()
this.currentVersion = 1
// here we remove the old permissions saved before adding 'permissionVersion'
// since with v1 the structure has been changed because of new engine ^0.2.0-alpha.6 changes
if (!localStorage.getItem('permissionVersion')) {
localStorage.setItem('plugins/permissions', '')
localStorage.setItem('permissionVersion', this.currentVersion.toString())
}
}
_getFromLocal() {
const permission = localStorage.getItem('plugins/permissions')
return permission ? JSON.parse(permission) : {}
}
persistPermissions() {
const permissions = JSON.stringify(this.permissions)
localStorage.setItem('plugins/permissions', permissions)
}
switchMode (from: Profile, to: Profile, method: string, set: boolean) {
set
? this.permissions[to.name][method][from.name] = {}
: delete this.permissions[to.name][method][from.name]
}
clear() {
localStorage.removeItem('plugins/permissions')
}
notAllowWarning(from: Profile, to: Profile, method: string) {
return `${from.displayName || from.name} is not allowed to call ${method} method of ${to.displayName || to.name}.`
}
async getTheme() {
return (await this.call('theme', 'currentTheme')).quality
}
/**
* Check if a plugin has the permission to call another plugin and askPermission if needed
* @param {PluginProfile} from the profile of the plugin that make the call
* @param {ModuleProfile} to The profile of the module that receive the call
* @param {string} method The name of the function to be called
* @param {string} message from the caller plugin to add more details if needed
* @returns {Promise<boolean>}
*/
async askPermission(from: Profile, to: Profile, method: string, message: string) {
try {
this.permissions = this._getFromLocal()
if (!this.permissions[to.name]) this.permissions[to.name] = {}
if (!this.permissions[to.name][method]) this.permissions[to.name][method] = {}
if (!this.permissions[to.name][method][from.name]) return this.openPermission(from, to, method, message)
const { allow, hash } = this.permissions[to.name][method][from.name]
if (!allow) {
const warning = this.notAllowWarning(from, to, method)
this.call('notification', 'toast', warning)
return false
}
return hash === from.hash
? true // Allow
: await this.openPermission(from, to, method, message)
} catch (err) {
throw new Error(err)
}
}
async openPermission(from: Profile, to: Profile, method: string, message: string) {
const remember = this.permissions[to.name][method][from.name]
const value: PermissionHandlerValue = {
from,
to,
method,
message,
remember
}
const modal: AppModal = {
id: 'PermissionHandler',
title: `Permission needed for ${to.displayName || to.name}`,
message: <PermissionHandlerDialog plugin={this} theme={await this.getTheme()} value={value}></PermissionHandlerDialog>,
okLabel: 'Accept',
cancelLabel: 'Decline'
}
const result = await this.call('notification', 'modal', modal)
return new Promise((resolve, reject) => {
if (result) {
if (this.permissions[to.name][method][from.name]) {
this.permissions[to.name][method][from.name] = {
allow: true,
hash: from.hash
}
this.persistPermissions()
}
resolve(true)
} else {
if (this.permissions[to.name][method][from.name]) {
this.permissions[to.name][method][from.name] = {
allow: false,
hash: from.hash
}
this.persistPermissions()
}
reject(this.notAllowWarning(from, to, method))
}
})
}
}

@ -1,24 +1,13 @@
/* eslint-disable no-unused-vars */
import React, { useRef, useState, useEffect } from 'react' // eslint-disable-line
import isElectron from 'is-electron'
import { WebsocketPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json'
import { version as remixdVersion } from '../../../../../libs/remixd/package.json'
var yo = require('yo-yo')
var modalDialog = require('../ui/modaldialog')
var modalDialogCustom = require('../ui/modal-dialog-custom')
var copyToClipboard = require('../ui/copy-to-clipboard')
import { PluginManager } from '@remixproject/engine'
import { AppModal, AlertModal } from '@remix-ui/app'
import { CopyToClipboard } from '@remix-ui/clipboard'
var csjs = require('csjs-inject')
var css = csjs`
.dialog {
display: flex;
flex-direction: column;
}
.dialogParagraph {
margin-bottom: 2em;
word-break: break-word;
}
`
const LOCALHOST = ' - connect to localhost - '
const profile = {
@ -32,29 +21,37 @@ const profile = {
version: packageJson.version
}
enum State {
ok,
cancel,
new
}
export class RemixdHandle extends WebsocketPlugin {
localhostProvider: any
appManager: PluginManager
constructor (localhostProvider, appManager) {
super(profile)
this.localhostProvider = localhostProvider
this.appManager = appManager
}
deactivate () {
async deactivate () {
if (super.socket) super.deactivate()
// this.appManager.deactivatePlugin('git') // plugin call doesn't work.. see issue https://github.com/ethereum/remix-plugin/issues/342
if (this.appManager.actives.includes('hardhat')) this.appManager.deactivatePlugin('hardhat')
if (this.appManager.actives.includes('slither')) this.appManager.deactivatePlugin('slither')
if (this.appManager.isActive('hardhat')) this.appManager.deactivatePlugin('hardhat')
if (this.appManager.isActive('slither')) this.appManager.deactivatePlugin('slither')
this.localhostProvider.close((error) => {
if (error) console.log(error)
})
}
activate () {
async activate () {
this.connectToLocalhost()
return true
}
async canceled () {
// await this.appManager.deactivatePlugin('git') // plugin call doesn't work.. see issue https://github.com/ethereum/remix-plugin/issues/342
await this.appManager.deactivatePlugin('remixd')
}
@ -65,23 +62,25 @@ export class RemixdHandle extends WebsocketPlugin {
* @param {String} txHash - hash of the transaction
*/
async connectToLocalhost () {
const connection = (error) => {
const connection = (error?:any) => {
if (error) {
console.log(error)
modalDialogCustom.alert(
'Cannot connect to the remixd daemon. ' +
'Please make sure you have the remixd running in the background.'
)
const alert:AlertModal = {
id: 'connectionAlert',
message: 'Cannot connect to the remixd daemon. Please make sure you have the remixd running in the background.'
}
this.call('notification', 'alert', alert)
this.canceled()
} else {
const intervalId = setInterval(() => {
if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed
clearInterval(intervalId)
console.log(error)
modalDialogCustom.alert(
'Connection to remixd terminated. ' +
'Please make sure remixd is still running in the background.'
)
const alert:AlertModal = {
id: 'connectionAlert',
message: 'Connection to remixd terminated.Please make sure remixd is still running in the background.'
}
this.call('notification', 'alert', alert)
this.canceled()
}
}, 3000)
@ -96,34 +95,32 @@ export class RemixdHandle extends WebsocketPlugin {
this.deactivate()
} else if (!isElectron()) {
// warn the user only if he/she is in the browser context
modalDialog(
'Connect to localhost',
remixdDialog(),
{
label: 'Connect',
fn: () => {
try {
this.localhostProvider.preInit()
super.activate()
setTimeout(() => {
if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed
connection(new Error('Connection with daemon failed.'))
} else {
connection()
}
}, 3000)
} catch (error) {
connection(error)
}
}
},
{
label: 'Cancel',
fn: () => {
this.canceled()
const mod:AppModal = {
id: 'remixdConnect',
title: 'Connect to localhost',
message: remixdDialog(),
okLabel: 'Connect',
cancelLabel: 'Cancel',
}
const result = await this.call('notification', 'modal', mod)
if(result) {
try {
this.localhostProvider.preInit()
super.activate()
setTimeout(() => {
if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed
connection(new Error('Connection with daemon failed.'))
} else {
connection()
}
}, 3000)
} catch (error) {
connection(error)
}
}
)
}
else {
await this.canceled()
}
} else {
try {
super.activate()
@ -137,31 +134,31 @@ export class RemixdHandle extends WebsocketPlugin {
function remixdDialog () {
const commandText = 'remixd -s <path-to-the-shared-folder> -u <remix-ide-instance-URL>'
return yo`
<div class=${css.dialog}>
<div class=${css.dialogParagraph}>
Access your local file system from Remix IDE using <a target="_blank" href="https://www.npmjs.com/package/@remix-project/remixd">Remixd NPM package</a>.<br/><br/>
return (<>
<div className=''>
<div className='mb-2 text-break'>
Access your local file system from Remix IDE using <a target="_blank" href="https://www.npmjs.com/package/@remix-project/remixd">Remixd NPM package</a>.<br/><br/>
Remixd needs to be running in the background to load the files in localhost workspace. For more info, please check the <a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html">Remixd tutorial</a>.
</div>
<div class=${css.dialogParagraph}>
<div className='mb-2 text-break'>
If you are just looking for the remixd command, here it is:
<br><br><b>${commandText}</b>
<span class="">${copyToClipboard(() => commandText)}</span>
<br></br><br></br><b>{commandText}</b>
<CopyToClipboard data-id='remixdCopyCommand' content={commandText}></CopyToClipboard>
</div>
<div class=${css.dialogParagraph}>
When connected, a session will be started between <em>${window.location.origin}</em> and your local file system at <i>ws://127.0.0.1:65520</i>.
The shared folder will be in the "File Explorers" workspace named "localhost".
<div className='mb-2 text-break'>
When connected, a session will be started between <em>{window.location.origin}</em> and your local file system at <i>ws://127.0.0.1:65520</i>.
The shared folder will be in the "File Explorers" workspace named "localhost".
<br/>Read more about other <a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html#ports-usage">Remixd ports usage</a>
</div>
<div class=${css.dialogParagraph}>
<div className='mb-2 text-break'>
This feature is still in Alpha. We recommend to keep a backup of the shared folder.
</div>
<div class=${css.dialogParagraph}>
<h6 class="text-danger">
Before using, make sure remixd version is latest i.e. <b>${remixdVersion}</b>
<br><a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html#update-to-the-latest-remixd">Read here how to update it</a>
<div className='mb-2 text-break'>
<h6 className="text-danger">
Before using, make sure remixd version is latest i.e. <b>v{remixdVersion}</b>
<br></br><a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html#update-to-the-latest-remixd">Read here how to update it</a>
</h6>
</div>
</div>
`
</>)
}

@ -0,0 +1,38 @@
type registryEntry = {
api: any,
name: string
}
export default class Registry {
private static instance: Registry;
private state: any
private constructor () {
this.state = {}
}
public static getInstance (): Registry {
if (!Registry.instance) {
Registry.instance = new Registry()
}
return Registry.instance
}
public put (entry: registryEntry) {
if (this.state[entry.name]) return this.state[entry.name]
const server = {
// uid: serveruid,
api: entry.api
}
this.state[entry.name] = { server }
return server
}
public get (name: string) {
const state = this.state[name]
if (!state) return
const server = state.server
return server
}
}

@ -4,7 +4,7 @@ import ReactDOM from 'react-dom'
import { EventEmitter } from 'events'
import {RemixUiStaticAnalyser} from '@remix-ui/static-analyser' // eslint-disable-line
import * as packageJson from '../../../../../package.json'
var Renderer = require('../ui/renderer')
import Registry from '../state/registry'
var EventManager = require('../../lib/events')
@ -22,16 +22,14 @@ const profile = {
}
class AnalysisTab extends ViewPlugin {
constructor (registry) {
constructor () {
super(profile)
this.event = new EventManager()
this.events = new EventEmitter()
this.registry = registry
this.registry = Registry.getInstance()
this.element = document.createElement('div')
this.element.setAttribute('id', 'staticAnalyserView')
this._components = {
renderer: new Renderer(this)
}
this._components = {}
this._components.registry = this.registry
this._deps = {
offsetToLineColumnConverter: this.registry.get(

@ -8,11 +8,7 @@ import { ViewPlugin } from '@remixproject/engine-web'
import QueryParams from '../../lib/query-params'
// import { ICompilerApi } from '@remix-project/remix-lib-ts'
import * as packageJson from '../../../../../package.json'
const yo = require('yo-yo')
const addTooltip = require('../ui/tooltip')
const css = require('./styles/compile-tab-styles')
import { compilerConfigChangedToastMsg, compileToastMsg } from '@remix-ui/helper'
const profile = {
name: 'solidity',
@ -41,6 +37,8 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA
this.compiler = this.compileTabLogic.compiler
this.compileTabLogic.init()
this.initCompilerApi()
this.el = document.createElement('div')
this.el.setAttribute('id', 'compileTabView')
}
renderComponent () {
@ -70,11 +68,6 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA
}
render () {
if (this.el) return this.el
this.el = yo`
<div class="${css.debuggerTabView}" id="compileTabView">
<div id="compiler" class="${css.compiler}"></div>
</div>`
this.renderComponent()
return this.el
@ -101,11 +94,13 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA
super.setCompilerConfig(settings)
this.renderComponent()
// @todo(#2875) should use loading compiler return value to check whether the compiler is loaded instead of "setInterval"
addTooltip(yo`<div><b>${this.currentRequest.from}</b> is updating the <b>Solidity compiler configuration</b>.<pre class="text-left">${JSON.stringify(settings, null, '\t')}</pre></div>`)
const value = JSON.stringify(settings, null, '\t')
this.call('notification', 'toast', compilerConfigChangedToastMsg(this.currentRequest.from, value))
}
compile (fileName) {
addTooltip(yo`<div><b>${this.currentRequest.from}</b> is requiring to compile <b>${fileName}</b></div>`)
this.call('notification', 'toast', compileToastMsg(this.currentRequest.from, fileName))
super.compile(fileName)
}
@ -144,17 +139,12 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA
this.queryParams.update(params)
}
getAppParameter (name) {
// first look in the URL params then in the local storage
const params = this.queryParams.get()
const param = params[name] ? params[name] : this.config.get(name)
if (param === 'true') return true
if (param === 'false') return false
return param
async getAppParameter (name) {
return await this.call('config', 'getAppParameter', name)
}
setAppParameter (name, value) {
this.config.set(name, value)
async setAppParameter (name, value) {
await this.call('config', 'setAppParameter', name, value)
}
}

@ -1,14 +1,12 @@
import toaster from '../ui/tooltip'
import { DebuggerUI } from '@remix-ui/debugger-ui' // eslint-disable-line
import { DebuggerApiMixin } from '@remixproject/debugger-plugin'
import { ViewPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json'
import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
import modalDialogCustom from '../ui/modal-dialog-custom'
import * as remixBleach from '../../lib/remixBleach'
import { compilationFinishedToastMsg, compilingToastMsg, localCompilationToastMsg, notFoundToastMsg, sourceVerificationNotAvailableToastMsg } from '@remix-ui/helper'
const css = require('./styles/debugger-tab-styles')
const yo = require('yo-yo')
const profile = {
name: 'debugger',
@ -26,46 +24,45 @@ const profile = {
export class DebuggerTab extends DebuggerApiMixin(ViewPlugin) {
constructor () {
super(profile)
this.el = null
this.el = document.createElement('div')
this.el.setAttribute('id', 'debugView')
this.el.classList.add(css.debuggerTabView)
this.initDebuggerApi()
}
render () {
if (this.el) return this.el
this.el = yo`
<div class="${css.debuggerTabView}" id="debugView">
<div id="debugger" class="${css.debugger}"></div>
</div>`
this.on('fetchAndCompile', 'compiling', (settings) => {
toaster(yo`<div><b>Recompiling and debugging with params</b><pre class="text-left">${JSON.stringify(settings, null, '\t')}</pre></div>`)
settings = JSON.stringify(settings, null, '\t')
this.call('notification', 'toast', compilingToastMsg(settings))
})
this.on('fetchAndCompile', 'compilationFailed', (data) => {
toaster(yo`<div><b>Compilation failed...</b> continuing <i>without</i> source code debugging.</div>`)
this.call('notification', 'toast', compilationFinishedToastMsg())
})
this.on('fetchAndCompile', 'notFound', (contractAddress) => {
toaster(yo`<div><b>Contract ${contractAddress} not found in source code repository</b> continuing <i>without</i> source code debugging.</div>`)
this.call('notification', 'toast', notFoundToastMsg(contractAddress))
})
this.on('fetchAndCompile', 'usingLocalCompilation', (contractAddress) => {
toaster(yo`<div><b>Using compilation result from Solidity module</b></div>`)
this.call('notification', 'toast', localCompilationToastMsg())
})
this.on('fetchAndCompile', 'sourceVerificationNotAvailable', () => {
toaster(yo`<div><b>Source verification plugin not activated or not available.</b> continuing <i>without</i> source code debugging.</div>`)
this.call('notification', 'toast', sourceVerificationNotAvailableToastMsg())
})
this.renderComponent()
return this.el
}
showMessage (title, message) {
try {
modalDialogCustom.alert(title, remixBleach.sanitize(message))
this.call('notification', 'alert', {
id: 'debuggerTabShowMessage',
title,
message: remixBleach.sanitize(message)
})
} catch (e) {
console.log(e)
}

@ -1,82 +0,0 @@
import * as packageJson from '../../../../../package.json'
import { Plugin } from '@remixproject/engine'
import Web3 from 'web3'
const yo = require('yo-yo')
const modalDialogCustom = require('../ui/modal-dialog-custom')
const profile = {
name: 'hardhat-provider',
displayName: 'Hardhat Provider',
kind: 'provider',
description: 'Hardhat provider',
methods: ['sendAsync'],
version: packageJson.version
}
export default class HardhatProvider extends Plugin {
constructor (blockchain) {
super(profile)
this.provider = null
this.blocked = false // used to block any call when trying to recover after a failed connection.
this.blockchain = blockchain
}
onDeactivation () {
this.provider = null
this.blocked = false
}
hardhatProviderDialogBody () {
return yo`
<div class="">
Note: To run Hardhat network node on your system, go to hardhat project folder and run command:
<div class="border p-1">npx hardhat node</div>
<br>
For more info, visit: <a href="https://hardhat.org/getting-started/#connecting-a-wallet-or-dapp-to-hardhat-network" target="_blank">Hardhat Documentation</a>
<br><br>
Hardhat JSON-RPC Endpoint
</div>
`
}
sendAsync (data) {
return new Promise((resolve, reject) => {
if (this.blocked) return reject(new Error('provider unable to connect'))
// If provider is not set, allow to open modal only when provider is trying to connect
if (!this.provider) {
modalDialogCustom.prompt('Hardhat node request', this.hardhatProviderDialogBody(), 'http://127.0.0.1:8545', (target) => {
this.provider = new Web3.providers.HttpProvider(target)
this.sendAsyncInternal(data, resolve, reject)
}, () => {
this.sendAsyncInternal(data, resolve, reject)
})
} else {
this.sendAsyncInternal(data, resolve, reject)
}
})
}
sendAsyncInternal (data, resolve, reject) {
if (this.provider) {
// Check the case where current environment is VM on UI and it still sends RPC requests
// This will be displayed on UI tooltip as 'cannot get account list: Environment Updated !!'
if (this.blockchain.getProvider() !== 'Hardhat Provider' && data.method !== 'net_listening') return reject(new Error('Environment Updated !!'))
this.provider[this.provider.sendAsync ? 'sendAsync' : 'send'](data, async (error, message) => {
if (error) {
this.blocked = true
modalDialogCustom.alert('Hardhat Provider', `Error while connecting to the hardhat provider: ${error.message}`)
await this.call('udapp', 'setEnvironmentMode', { context: 'vm', fork: 'london' })
this.provider = null
setTimeout(_ => { this.blocked = false }, 1000) // we wait 1 second for letting remix to switch to vm
return reject(error)
}
resolve(message)
})
} else {
const result = data.method === 'net_listening' ? 'canceled' : []
resolve({ jsonrpc: '2.0', result: result, id: data.id })
}
}
}
module.exports = HardhatProvider

@ -0,0 +1,128 @@
import * as packageJson from '../../../../../package.json'
import { Plugin } from '@remixproject/engine'
import { AppModal, AlertModal, ModalTypes } from '@remix-ui/app'
import React from 'react' // eslint-disable-line
import { Blockchain } from '../../blockchain/blockchain'
import { ethers } from 'ethers'
const profile = {
name: 'hardhat-provider',
displayName: 'Hardhat Provider',
kind: 'provider',
description: 'Hardhat provider',
methods: ['sendAsync'],
version: packageJson.version
}
type JsonDataRequest = {
id: number,
jsonrpc: string // version
method: string,
params: Array<any>,
}
type JsonDataResult = {
id: number,
jsonrpc: string // version
result: any
}
type RejectRequest = (error: Error) => void
type SuccessRequest = (data: JsonDataResult) => void
export class HardhatProvider extends Plugin {
provider: ethers.providers.JsonRpcProvider
blocked: boolean
blockchain: Blockchain
target: String
constructor (blockchain) {
super(profile)
this.provider = null
this.blocked = false // used to block any call when trying to recover after a failed connection.
this.blockchain = blockchain
}
onDeactivation () {
this.provider = null
this.blocked = false
}
hardhatProviderDialogBody (): JSX.Element {
return (<div> Note: To run Hardhat network node on your system, go to hardhat project folder and run command:
<div className="border p-1">npx hardhat node</div>
For more info, visit: <a href="https://hardhat.org/getting-started/#connecting-a-wallet-or-dapp-to-hardhat-network" target="_blank">Hardhat Documentation</a>
Hardhat JSON-RPC Endpoint
</div>)
}
sendAsync (data: JsonDataRequest): Promise<any> {
return new Promise(async (resolve, reject) => {
if (this.blocked) return reject(new Error('provider unable to connect'))
// If provider is not set, allow to open modal only when provider is trying to connect
if (!this.provider) {
let value: string
try {
value = await ((): Promise<string> => {
return new Promise((resolve, reject) => {
const modalContent: AppModal = {
id: 'hardhatprovider',
title: 'Hardhat node request',
message: this.hardhatProviderDialogBody(),
modalType: ModalTypes.prompt,
okLabel: 'OK',
cancelLabel: 'Cancel',
okFn: (value: string) => {
setTimeout(() => resolve(value), 0)
},
cancelFn: () => {
setTimeout(() => reject(new Error('Canceled')), 0)
},
hideFn: () => {
setTimeout(() => reject(new Error('Hide')), 0)
},
defaultValue: 'http://127.0.0.1:8545'
}
this.call('notification', 'modal', modalContent)
})
})()
} catch (e) {
// the modal has been canceled/hide
return
}
this.provider = new ethers.providers.JsonRpcProvider(value)
this.sendAsyncInternal(data, resolve, reject)
} else {
this.sendAsyncInternal(data, resolve, reject)
}
})
}
private async sendAsyncInternal (data: JsonDataRequest, resolve: SuccessRequest, reject: RejectRequest): Promise<void> {
if (this.provider) {
// Check the case where current environment is VM on UI and it still sends RPC requests
// This will be displayed on UI tooltip as 'cannot get account list: Environment Updated !!'
if (this.blockchain.getProvider() !== 'Hardhat Provider' && data.method !== 'net_listening') return reject(new Error('Environment Updated !!'))
try {
const result = await this.provider.send(data.method, data.params)
resolve({ jsonrpc: '2.0', result, id: data.id })
} catch (error) {
this.blocked = true
const modalContent: AlertModal = {
id: 'hardhatprovider',
title: 'Hardhat Provider',
message: `Error while connecting to the hardhat provider: ${error.message}`,
}
this.call('notification', 'alert', modalContent)
await this.call('udapp', 'setEnvironmentMode', { context: 'vm', fork: 'london' })
this.provider = null
setTimeout(_ => { this.blocked = false }, 1000) // we wait 1 second for letting remix to switch to vm
reject(error)
}
} else {
const result = data.method === 'net_listening' ? 'canceled' : []
resolve({ jsonrpc: '2.0', result: result, id: data.id })
}
}
}

@ -1,22 +0,0 @@
var yo = require('yo-yo')
var css = require('./styles/plugin-tab-styles')
class PluginTab {
constructor (json) {
this.el = null
this.data = { json }
}
render () {
if (this.el) return this.el
this.el = yo`
<div class="${css.pluginTabView}" id="pluginView">
<iframe class="${css.iframe}" src="${this.data.json.url}/index.html"></iframe>
</div>`
return this.el
}
}
module.exports = PluginTab

@ -1,423 +0,0 @@
import publishToStorage from '../../../publishToStorage'
const yo = require('yo-yo')
const ethJSUtil = require('ethereumjs-util')
const css = require('../styles/run-tab-styles')
const modalDialogCustom = require('../../ui/modal-dialog-custom')
const remixLib = require('@remix-project/remix-lib')
const EventManager = remixLib.EventManager
const confirmDialog = require('../../ui/confirmDialog')
const modalDialog = require('../../ui/modaldialog')
const MultiParamManager = require('../../ui/multiParamManager')
const helper = require('../../../lib/helper')
const addTooltip = require('../../ui/tooltip')
const _paq = window._paq = window._paq || []
class ContractDropdownUI {
constructor (blockchain, dropdownLogic, logCallback, runView) {
this.blockchain = blockchain
this.dropdownLogic = dropdownLogic
this.logCallback = logCallback
this.runView = runView
this.event = new EventManager()
this.listenToEvents()
this.ipfsCheckedState = false
this.exEnvironment = blockchain.getProvider()
this.listenToContextChange()
this.loadType = 'other'
}
listenToEvents () {
this.dropdownLogic.event.register('newlyCompiled', (success, data, source, compiler, compilerFullName, file) => {
if (!this.selectContractNames) return
this.selectContractNames.innerHTML = ''
if (success) {
this.dropdownLogic.getCompiledContracts(compiler, compilerFullName).forEach((contract) => {
this.selectContractNames.appendChild(yo`<option value="${contract.name}" compiler="${compilerFullName}">${contract.name} - ${contract.file}</option>`)
})
}
this.enableAtAddress(success)
this.enableContractNames(success)
this.setInputParamsPlaceHolder()
if (success) {
this.compFails.style.display = 'none'
} else {
this.compFails.style.display = 'block'
}
})
}
listenToContextChange () {
this.blockchain.event.register('networkStatus', ({ error, network }) => {
if (error) {
console.log('can\'t detect network')
return
}
this.exEnvironment = this.blockchain.getProvider()
this.networkName = network.name
this.networkId = network.id
const savedConfig = window.localStorage.getItem(`ipfs/${this.exEnvironment}/${this.networkName}`)
// check if an already selected option exist else use default workflow
if (savedConfig !== null) {
this.setCheckedState(savedConfig)
} else {
this.setCheckedState(this.networkName === 'Main')
}
})
}
setCheckedState (value) {
value = value === 'true' ? true : value === 'false' ? false : value
this.ipfsCheckedState = value
if (this.ipfsCheckbox) this.ipfsCheckbox.checked = value
}
toggleCheckedState () {
if (this.exEnvironment === 'vm') this.networkName = 'VM'
this.ipfsCheckedState = !this.ipfsCheckedState
window.localStorage.setItem(`ipfs/${this.exEnvironment}/${this.networkName}`, this.ipfsCheckedState)
}
enableContractNames (enable) {
if (enable) {
if (this.selectContractNames.value === '') return
this.selectContractNames.removeAttribute('disabled')
this.selectContractNames.setAttribute('title', 'Select contract for Deploy or At Address.')
} else {
this.selectContractNames.setAttribute('disabled', true)
if (this.loadType === 'sol') {
this.selectContractNames.setAttribute('title', '⚠ Select and compile *.sol file to deploy or access a contract.')
} else {
this.selectContractNames.setAttribute('title', '⚠ Selected *.abi file allows accessing contracts, select and compile *.sol file to deploy and access one.')
}
}
}
enableAtAddress (enable) {
if (enable) {
const address = this.atAddressButtonInput.value
if (!address || !ethJSUtil.isValidAddress(address)) {
this.enableAtAddress(false)
return
}
this.atAddress.removeAttribute('disabled')
this.atAddress.setAttribute('title', 'Interact with the given contract.')
} else {
this.atAddress.setAttribute('disabled', true)
if (this.atAddressButtonInput.value === '') {
this.atAddress.setAttribute('title', '⚠ Compile *.sol file or select *.abi file & then enter the address of deployed contract.')
} else {
this.atAddress.setAttribute('title', '⚠ Compile *.sol file or select *.abi file.')
}
}
}
render () {
this.compFails = yo`<i title="No contract compiled yet or compilation failed. Please check the compile tab for more information." class="m-2 ml-3 fas fa-times-circle ${css.errorIcon}" ></i>`
this.atAddress = yo`<button class="${css.atAddress} btn btn-sm btn-info" id="runAndDeployAtAdressButton" onclick=${this.loadFromAddress.bind(this)}>At Address</button>`
this.atAddressButtonInput = yo`<input class="${css.input} ${css.ataddressinput} ataddressinput form-control" placeholder="Load contract from Address" title="address of contract" oninput=${this.atAddressChanged.bind(this)} />`
this.selectContractNames = yo`<select class="${css.contractNames} custom-select" disabled title="Please compile *.sol file to deploy or access a contract"></select>`
this.abiLabel = yo`<span class="py-1">ABI file selected</span>`
if (this.exEnvironment === 'vm') this.networkName = 'VM'
this.enableAtAddress(false)
this.abiLabel.style.display = 'none'
const savedConfig = window.localStorage.getItem(`ipfs/${this.exEnvironment}/${this.networkName}`)
this.ipfsCheckedState = savedConfig === 'true' ? true : false // eslint-disable-line
this.ipfsCheckbox = yo`
<input
id="deployAndRunPublishToIPFS"
data-id="contractDropdownIpfsCheckbox"
class="form-check-input custom-control-input"
type="checkbox"
onchange=${() => this.toggleCheckedState()}
>
`
if (this.ipfsCheckedState) this.ipfsCheckbox.checked = true
this.deployCheckBox = yo`
<div class="d-flex py-1 align-items-center custom-control custom-checkbox">
${this.ipfsCheckbox}
<label
for="deployAndRunPublishToIPFS"
data-id="contractDropdownIpfsCheckboxLabel"
class="m-0 form-check-label custom-control-label ${css.checkboxAlign}"
title="Publishing the source code and metadata to IPFS facilitates source code verification using Sourcify and will greatly foster contract adoption (auditing, debugging, calling it, etc...)"
>
Publish to IPFS
</label>
</div>
`
this.createPanel = yo`<div class="${css.deployDropdown}"></div>`
this.orLabel = yo`<div class="${css.orLabel} mt-2">or</div>`
const contractNamesContainer = yo`
<div class="${css.container}" data-id="contractDropdownContainer">
<label class="${css.settingsLabel}">Contract</label>
<div class="${css.subcontainer}">
${this.selectContractNames} ${this.compFails}
${this.abiLabel}
</div>
<div>
${this.createPanel}
${this.orLabel}
<div class="${css.button} ${css.atAddressSect}">
${this.atAddress}
${this.atAddressButtonInput}
</div>
</div>
</div>
`
this.selectContractNames.addEventListener('change', this.setInputParamsPlaceHolder.bind(this))
this.setInputParamsPlaceHolder()
if (!this.contractNamesContainer) {
this.contractNamesContainer = contractNamesContainer
}
return contractNamesContainer
}
atAddressChanged (event) {
if (!this.atAddressButtonInput.value) {
this.enableAtAddress(false)
} else {
if ((this.selectContractNames && !this.selectContractNames.getAttribute('disabled') && this.loadType === 'sol') ||
this.loadType === 'abi') {
this.enableAtAddress(true)
} else {
this.enableAtAddress(false)
}
}
}
changeCurrentFile (currentFile) {
if (!this.selectContractNames) return
if (/.(.abi)$/.exec(currentFile)) {
this.createPanel.style.display = 'none'
this.orLabel.style.display = 'none'
this.compFails.style.display = 'none'
this.loadType = 'abi'
this.contractNamesContainer.style.display = 'block'
this.abiLabel.style.display = 'block'
this.abiLabel.innerHTML = currentFile
this.selectContractNames.style.display = 'none'
this.enableContractNames(true)
this.enableAtAddress(true)
} else if (/.(.sol)$/.exec(currentFile) ||
/.(.vy)$/.exec(currentFile) || // vyper
/.(.lex)$/.exec(currentFile) || // lexon
/.(.contract)$/.exec(currentFile)) {
this.createPanel.style.display = 'block'
this.orLabel.style.display = 'block'
this.contractNamesContainer.style.display = 'block'
this.loadType = 'sol'
this.selectContractNames.style.display = 'block'
this.abiLabel.style.display = 'none'
if (this.selectContractNames.value === '') this.enableAtAddress(false)
} else {
this.loadType = 'other'
this.createPanel.style.display = 'block'
this.orLabel.style.display = 'block'
this.contractNamesContainer.style.display = 'block'
this.selectContractNames.style.display = 'block'
this.abiLabel.style.display = 'none'
if (this.selectContractNames.value === '') this.enableAtAddress(false)
}
}
setInputParamsPlaceHolder () {
this.createPanel.innerHTML = ''
if (this.selectContractNames.selectedIndex < 0 || this.selectContractNames.children.length <= 0) {
this.createPanel.innerHTML = 'No compiled contracts'
return
}
const selectedContract = this.getSelectedContract()
const clickCallback = async (valArray, inputsValues) => {
var selectedContract = this.getSelectedContract()
this.createInstance(selectedContract, inputsValues)
}
const createConstructorInstance = new MultiParamManager(
0,
selectedContract.getConstructorInterface(),
clickCallback,
selectedContract.getConstructorInputs(),
'Deploy',
selectedContract.bytecodeObject,
true
)
this.createPanel.appendChild(createConstructorInstance.render())
this.createPanel.appendChild(this.deployCheckBox)
}
getSelectedContract () {
var contract = this.selectContractNames.children[this.selectContractNames.selectedIndex]
var contractName = contract.getAttribute('value')
var compilerAtributeName = contract.getAttribute('compiler')
return this.dropdownLogic.getSelectedContract(contractName, compilerAtributeName)
}
async createInstance (selectedContract, args) {
if (selectedContract.bytecodeObject.length === 0) {
return modalDialogCustom.alert('This contract may be abstract, not implement an abstract parent\'s methods completely or not invoke an inherited contract\'s constructor correctly.')
}
var continueCb = (error, continueTxExecution, cancelCb) => {
if (error) {
var msg = typeof error !== 'string' ? error.message : error
modalDialog('Gas estimation failed', yo`<div>Gas estimation errored with the following message (see below).
The transaction execution will likely fail. Do you want to force sending? <br>
${msg}
</div>`,
{
label: 'Send Transaction',
fn: () => {
continueTxExecution()
}
}, {
label: 'Cancel Transaction',
fn: () => {
cancelCb()
}
})
} else {
continueTxExecution()
}
}
const self = this
var promptCb = (okCb, cancelCb) => {
modalDialogCustom.promptPassphrase('Passphrase requested', 'Personal mode is enabled. Please provide passphrase of account', '', okCb, cancelCb)
}
var statusCb = (msg) => {
return this.logCallback(msg)
}
var finalCb = (error, contractObject, address) => {
self.event.trigger('clearInstance')
if (error) {
return this.logCallback(error)
}
self.event.trigger('newContractInstanceAdded', [contractObject, address, contractObject.name])
const data = self.runView.compilersArtefacts.getCompilerAbstract(contractObject.contract.file)
self.runView.compilersArtefacts.addResolvedContract(helper.addressToString(address), data)
if (self.ipfsCheckedState) {
_paq.push(['trackEvent', 'udapp', 'DeployAndPublish', this.networkName + '_' + this.networkId])
publishToStorage('ipfs', self.runView.fileProvider, self.runView.fileManager, selectedContract)
} else {
_paq.push(['trackEvent', 'udapp', 'DeployOnly', this.networkName + '_' + this.networkId])
}
}
let contractMetadata
try {
contractMetadata = await this.runView.call('compilerMetadata', 'deployMetadataOf', selectedContract.name, selectedContract.contract.file)
} catch (error) {
return statusCb(`creation of ${selectedContract.name} errored: ${error.message ? error.message : error}`)
}
const compilerContracts = this.dropdownLogic.getCompilerContracts()
const confirmationCb = this.getConfirmationCb(modalDialog, confirmDialog)
if (selectedContract.isOverSizeLimit()) {
return modalDialog('Contract code size over limit', yo`<div>Contract creation initialization returns data with length of more than 24576 bytes. The deployment will likely fails. <br>
More info: <a href="https://github.com/ethereum/EIPs/blob/master/EIPS/eip-170.md" target="_blank">eip-170</a>
</div>`,
{
label: 'Force Send',
fn: () => {
this.deployContract(selectedContract, args, contractMetadata, compilerContracts, { continueCb, promptCb, statusCb, finalCb }, confirmationCb)
}
}, {
label: 'Cancel',
fn: () => {
this.logCallback(`creation of ${selectedContract.name} canceled by user.`)
}
})
}
this.deployContract(selectedContract, args, contractMetadata, compilerContracts, { continueCb, promptCb, statusCb, finalCb }, confirmationCb)
}
deployContract (selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) {
_paq.push(['trackEvent', 'udapp', 'DeployContractTo', this.networkName + '_' + this.networkId])
const { statusCb } = callbacks
if (!contractMetadata || (contractMetadata && contractMetadata.autoDeployLib)) {
return this.blockchain.deployContractAndLibraries(selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb)
}
if (Object.keys(selectedContract.bytecodeLinkReferences).length) statusCb(`linking ${JSON.stringify(selectedContract.bytecodeLinkReferences, null, '\t')} using ${JSON.stringify(contractMetadata.linkReferences, null, '\t')}`)
this.blockchain.deployContractWithLibrary(selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb)
}
getConfirmationCb (modalDialog, confirmDialog) {
// this code is the same as in recorder.js. TODO need to be refactored out
const confirmationCb = (network, tx, gasEstimation, continueTxExecution, cancelCb) => {
if (network.name !== 'Main') {
return continueTxExecution(null)
}
const amount = this.blockchain.fromWei(tx.value, true, 'ether')
const content = confirmDialog(tx, network, amount, gasEstimation, this.blockchain.determineGasFees(tx), this.blockchain.determineGasPrice.bind(this.blockchain))
modalDialog('Confirm transaction', content,
{
label: 'Confirm',
fn: () => {
this.blockchain.config.setUnpersistedProperty('doNotShowTransactionConfirmationAgain', content.querySelector('input#confirmsetting').checked)
// TODO: check if this is check is still valid given the refactor
if (!content.gasPriceStatus) {
cancelCb('Given transaction fee is not correct')
} else {
continueTxExecution(content.txFee)
}
}
}, {
label: 'Cancel',
fn: () => {
return cancelCb('Transaction canceled by user.')
}
}
)
}
return confirmationCb
}
loadFromAddress () {
this.event.trigger('clearInstance')
let address = this.atAddressButtonInput.value
if (!ethJSUtil.isValidChecksumAddress(address)) {
addTooltip(yo`
<span>
It seems you are not using a checksumed address.
<br>A checksummed address is an address that contains uppercase letters, as specified in <a href="https://eips.ethereum.org/EIPS/eip-55" target="_blank">EIP-55</a>.
<br>Checksummed addresses are meant to help prevent users from sending transactions to the wrong address.
</span>`)
address = ethJSUtil.toChecksumAddress(address)
}
this.dropdownLogic.loadContractFromAddress(address,
(cb) => {
modalDialogCustom.confirm('At Address', `Do you really want to interact with ${address} using the current ABI definition?`, cb)
},
(error, loadType, abi) => {
if (error) {
return modalDialogCustom.alert(error)
}
if (loadType === 'abi') {
return this.event.trigger('newContractABIAdded', [abi, address])
}
var selectedContract = this.getSelectedContract()
this.event.trigger('newContractInstanceAdded', [selectedContract.object, address, this.selectContractNames.value])
}
)
}
}
module.exports = ContractDropdownUI

@ -1,108 +0,0 @@
import { CompilerAbstract } from '@remix-project/remix-solidity'
const remixLib = require('@remix-project/remix-lib')
const txHelper = remixLib.execution.txHelper
const EventManager = remixLib.EventManager
const _paq = window._paq = window._paq || []
class DropdownLogic {
constructor (compilersArtefacts, config, editor, runView) {
this.compilersArtefacts = compilersArtefacts
this.config = config
this.editor = editor
this.runView = runView
this.event = new EventManager()
this.listenToCompilationEvents()
}
// TODO: can be moved up; the event in contractDropdown will have to refactored a method instead
listenToCompilationEvents () {
const broadcastCompilationResult = (file, source, languageVersion, data) => {
// TODO check whether the tab is configured
const compiler = new CompilerAbstract(languageVersion, data, source)
this.compilersArtefacts[languageVersion] = compiler
this.compilersArtefacts.__last = compiler
this.event.trigger('newlyCompiled', [true, data, source, compiler, languageVersion, file])
}
this.runView.on('solidity', 'compilationFinished', (file, source, languageVersion, data) =>
broadcastCompilationResult(file, source, languageVersion, data)
)
this.runView.on('vyper', 'compilationFinished', (file, source, languageVersion, data) =>
broadcastCompilationResult(file, source, languageVersion, data)
)
this.runView.on('lexon', 'compilationFinished', (file, source, languageVersion, data) =>
broadcastCompilationResult(file, source, languageVersion, data)
)
this.runView.on('yulp', 'compilationFinished', (file, source, languageVersion, data) =>
broadcastCompilationResult(file, source, languageVersion, data)
)
this.runView.on('optimism-compiler', 'compilationFinished', (file, source, languageVersion, data) =>
broadcastCompilationResult(file, source, languageVersion, data)
)
}
loadContractFromAddress (address, confirmCb, cb) {
if (/.(.abi)$/.exec(this.config.get('currentFile'))) {
confirmCb(() => {
var abi
try {
abi = JSON.parse(this.editor.currentContent())
} catch (e) {
return cb('Failed to parse the current file as JSON ABI.')
}
_paq.push(['trackEvent', 'udapp', 'AtAddressLoadWithABI'])
cb(null, 'abi', abi)
})
} else {
_paq.push(['trackEvent', 'udapp', 'AtAddressLoadWithArtifacts'])
cb(null, 'instance')
}
}
getCompiledContracts (compiler, compilerFullName) {
var contracts = []
compiler.visitContracts((contract) => {
contracts.push(contract)
})
return contracts
}
getSelectedContract (contractName, compilerAtributeName) {
if (!contractName) return null
var compiler = this.compilersArtefacts[compilerAtributeName]
if (!compiler) return null
var contract = compiler.getContract(contractName)
return {
name: contractName,
contract: contract,
compiler: compiler,
abi: contract.object.abi,
bytecodeObject: contract.object.evm.bytecode.object,
bytecodeLinkReferences: contract.object.evm.bytecode.linkReferences,
object: contract.object,
deployedBytecode: contract.object.evm.deployedBytecode,
getConstructorInterface: () => {
return txHelper.getConstructorInterface(contract.object.abi)
},
getConstructorInputs: () => {
var constructorInteface = txHelper.getConstructorInterface(contract.object.abi)
return txHelper.inputParametersDeclarationToString(constructorInteface.inputs)
},
isOverSizeLimit: () => {
var deployedBytecode = contract.object.evm.deployedBytecode
return (deployedBytecode && deployedBytecode.object.length / 2 > 24576)
},
metadata: contract.object.metadata
}
}
getCompilerContracts () {
return this.compilersArtefacts.__last.getData().contracts
}
}
module.exports = DropdownLogic

@ -259,7 +259,7 @@ class Recorder {
cb(err)
}
)
}, () => { self.setListen(true); self.clearAll() })
}, () => { self.setListen(true) })
}
runScenario (json, continueCb, promptCb, alertCb, confirmationCb, logCallBack, cb) {

@ -1,162 +0,0 @@
import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../../package.json'
var yo = require('yo-yo')
var remixLib = require('@remix-project/remix-lib')
var EventManager = remixLib.EventManager
var csjs = require('csjs-inject')
var css = require('../styles/run-tab-styles')
var modalDialogCustom = require('../../ui/modal-dialog-custom')
var modalDialog = require('../../ui/modaldialog')
var confirmDialog = require('../../ui/confirmDialog')
var helper = require('../../../lib/helper.js')
const profile = {
name: 'recorder',
methods: ['runScenario'],
version: packageJson.version
}
class RecorderUI extends Plugin {
constructor (blockchain, fileManager, recorder, logCallBack, config) {
super(profile)
this.fileManager = fileManager
this.blockchain = blockchain
this.recorder = recorder
this.logCallBack = logCallBack
this.config = config
this.event = new EventManager()
}
render () {
var css2 = csjs`
.container {}
.runTxs {}
.recorder {}
`
this.runButton = yo`<i class="fas fa-play runtransaction ${css2.runTxs} ${css.icon}" title="Run Transactions" aria-hidden="true"></i>`
this.recordButton = yo`
<i class="fas fa-save savetransaction ${css2.recorder} ${css.icon}"
onclick=${this.triggerRecordButton.bind(this)} title="Save Transactions" aria-hidden="true">
</i>`
this.runButton.onclick = () => {
const file = this.config.get('currentFile')
if (!file) return modalDialogCustom.alert('A scenario file has to be selected')
this.runScenario(file)
}
}
runScenario (file) {
if (!file) return modalDialogCustom.alert('Unable to run scenerio, no specified scenario file')
var continueCb = (error, continueTxExecution, cancelCb) => {
if (error) {
var msg = typeof error !== 'string' ? error.message : error
modalDialog('Gas estimation failed', yo`<div>Gas estimation errored with the following message (see below).
The transaction execution will likely fail. Do you want to force sending? <br>
${msg}
</div>`,
{
label: 'Send Transaction',
fn: () => {
continueTxExecution()
}
}, {
label: 'Cancel Transaction',
fn: () => {
cancelCb()
}
})
} else {
continueTxExecution()
}
}
var promptCb = (okCb, cancelCb) => {
modalDialogCustom.promptPassphrase('Passphrase requested', 'Personal mode is enabled. Please provide passphrase of account', '', okCb, cancelCb)
}
var alertCb = (msg) => {
modalDialogCustom.alert(msg)
}
const confirmationCb = this.getConfirmationCb(modalDialog, confirmDialog)
this.fileManager.readFile(file).then((json) => {
// TODO: there is still a UI dependency to remove here, it's still too coupled at this point to remove easily
this.recorder.runScenario(json, continueCb, promptCb, alertCb, confirmationCb, this.logCallBack, (error, abi, address, contractName) => {
if (error) {
return modalDialogCustom.alert(error)
}
this.event.trigger('newScenario', [abi, address, contractName])
})
}).catch((error) => modalDialogCustom.alert(error))
}
getConfirmationCb (modalDialog, confirmDialog) {
// this code is the same as in contractDropdown.js. TODO need to be refactored out
const confirmationCb = (network, tx, gasEstimation, continueTxExecution, cancelCb) => {
if (network.name !== 'Main') {
return continueTxExecution(null)
}
const amount = this.blockchain.fromWei(tx.value, true, 'ether')
const content = confirmDialog(tx, network, amount, gasEstimation, this.blockchain.determineGasFees(tx), this.blockchain.determineGasPrice.bind(this.blockchain))
modalDialog('Confirm transaction', content,
{
label: 'Confirm',
fn: () => {
this.config.setUnpersistedProperty('doNotShowTransactionConfirmationAgain', content.querySelector('input#confirmsetting').checked)
// TODO: check if this is check is still valid given the refactor
if (!content.gasPriceStatus) {
cancelCb('Given transaction fee is not correct')
} else {
continueTxExecution(content.txFee)
}
}
}, {
label: 'Cancel',
fn: () => {
return cancelCb('Transaction canceled by user.')
}
}
)
}
return confirmationCb
}
triggerRecordButton () {
this.saveScenario(
(path, cb) => {
modalDialogCustom.prompt('Save transactions as scenario', 'Transactions will be saved in a file under ' + path, 'scenario.json', cb)
},
(error) => {
if (error) return modalDialogCustom.alert(error)
}
)
}
async saveScenario (promptCb, cb) {
var txJSON = JSON.stringify(this.recorder.getAll(), null, 2)
var path = this.fileManager.currentPath()
promptCb(path, async input => {
var fileProvider = this.fileManager.fileProviderOf(path)
if (!fileProvider) return
var newFile = path + '/' + input
try {
newFile = await helper.createNonClashingNameAsync(newFile, this.fileManager)
await fileProvider.set(newFile, txJSON)
await this.fileManager.open(newFile)
} catch (error) {
if (error) return cb('Failed to create file. ' + newFile + ' ' + error)
}
})
}
}
module.exports = RecorderUI

@ -1,449 +0,0 @@
import { BN } from 'ethereumjs-util'
const $ = require('jquery')
const yo = require('yo-yo')
const remixLib = require('@remix-project/remix-lib')
const EventManager = remixLib.EventManager
const css = require('../styles/run-tab-styles')
const copyToClipboard = require('../../ui/copy-to-clipboard')
const modalDialogCustom = require('../../ui/modal-dialog-custom')
const addTooltip = require('../../ui/tooltip')
const helper = require('../../../lib/helper.js')
const globalRegistry = require('../../../global/registry')
class SettingsUI {
constructor (blockchain, networkModule) {
this.blockchain = blockchain
this.event = new EventManager()
this._components = {}
this.blockchain.event.register('transactionExecuted', (error, from, to, data, lookupOnly, txResult) => {
if (!lookupOnly) this.el.querySelector('#value').value = 0
if (error) return
this.updateAccountBalances()
})
this._components = {
registry: globalRegistry,
networkModule: networkModule
}
this._components.registry = globalRegistry
this._deps = {
config: this._components.registry.get('config').api
}
this._deps.config.events.on('settings/personal-mode_changed', this.onPersonalChange.bind(this))
setInterval(() => {
this.updateAccountBalances()
}, 1000)
this.accountListCallId = 0
this.loadedAccounts = {}
}
updateAccountBalances () {
if (!this.el) return
var accounts = $(this.el.querySelector('#txorigin')).children('option')
accounts.each((index, account) => {
this.blockchain.getBalanceInEther(account.value, (err, balance) => {
if (err) return
const updated = helper.shortenAddress(account.value, balance)
if (updated !== account.innerText) { // check if the balance has been updated and update UI accordingly.
account.innerText = updated
}
})
})
}
validateInputKey (e) {
// preventing not numeric keys
// preventing 000 case
if (!helper.isNumeric(e.key) ||
(e.key === '0' && !parseInt(this.el.querySelector('#value').value) && this.el.querySelector('#value').value.length > 0)) {
e.preventDefault()
e.stopImmediatePropagation()
}
}
validateValue () {
const valueEl = this.el.querySelector('#value')
if (!valueEl.value) {
// assign 0 if given value is
// - empty
valueEl.value = 0
return
}
let v
try {
v = new BN(valueEl.value, 10)
valueEl.value = v.toString(10)
} catch (e) {
// assign 0 if given value is
// - not valid (for ex 4345-54)
// - contains only '0's (for ex 0000) copy past or edit
valueEl.value = 0
}
// if giveen value is negative(possible with copy-pasting) set to 0
if (v.lt(0)) valueEl.value = 0
}
render () {
this.netUI = yo`<span class="${css.network} badge badge-secondary"></span>`
var environmentEl = yo`
<div class="${css.crow}">
<label id="selectExEnv" class="${css.settingsLabel}">
Environment
</label>
<div class="${css.environment}">
<select id="selectExEnvOptions" data-id="settingsSelectEnvOptions" class="form-control ${css.select} custom-select">
<option id="vm-mode-london" data-id="settingsVMLondonMode"
title="Execution environment does not connect to any node, everything is local and in memory only."
value="vm-london" name="executionContext" fork="london"> JavaScript VM (London)
</option>
<option id="vm-mode-berlin" data-id="settingsVMBerlinMode"
title="Execution environment does not connect to any node, everything is local and in memory only."
value="vm-berlin" name="executionContext" fork="berlin" > JavaScript VM (Berlin)
</option>
<option id="injected-mode" data-id="settingsInjectedMode"
title="Execution environment has been provided by Metamask or similar provider."
value="injected" name="executionContext"> Injected Web3
</option>
<option id="web3-mode" data-id="settingsWeb3Mode"
title="Execution environment connects to node at localhost (or via IPC if available), transactions will be sent to the network and can cause loss of money or worse!
If this page is served via https and you access your node via http, it might not work. In this case, try cloning the repository and serving it via http."
value="web3" name="executionContext"> Web3 Provider
</option>
</select>
<a href="https://remix-ide.readthedocs.io/en/latest/run.html#run-setup" target="_blank"><i class="${css.infoDeployAction} ml-2 fas fa-info" title="check out docs to setup Environment"></i></a>
</div>
</div>
`
const networkEl = yo`
<div class="${css.crow}">
<div class="${css.settingsLabel}">
</div>
<div class="${css.environment}" data-id="settingsNetworkEnv">
${this.netUI}
</div>
</div>
`
const accountEl = yo`
<div class="${css.crow}">
<label class="${css.settingsLabel}">
Account
<span id="remixRunPlusWraper" title="Create a new account" onload=${this.updatePlusButton.bind(this)}>
<i id="remixRunPlus" class="fas fa-plus-circle ${css.icon}" aria-hidden="true" onclick=${this.newAccount.bind(this)}"></i>
</span>
</label>
<div class="${css.account}">
<select data-id="runTabSelectAccount" name="txorigin" class="form-control ${css.select} custom-select pr-4" id="txorigin"></select>
<div style="margin-left: -5px;">${copyToClipboard(() => document.querySelector('#runTabView #txorigin').value)}</div>
<i id="remixRunSignMsg" data-id="settingsRemixRunSignMsg" class="mx-1 fas fa-edit ${css.icon}" aria-hidden="true" onclick=${this.signMessage.bind(this)} title="Sign a message using this account key"></i>
</div>
</div>
`
const gasPriceEl = yo`
<div class="${css.crow}">
<label class="${css.settingsLabel}">Gas limit</label>
<input type="number" class="form-control ${css.gasNval} ${css.col2}" id="gasLimit" value="3000000">
</div>
`
const valueEl = yo`
<div class="${css.crow}">
<label class="${css.settingsLabel}" data-id="remixDRValueLabel">Value</label>
<div class="${css.gasValueContainer}">
<input
type="number"
min="0"
pattern="^[0-9]"
step="1"
class="form-control ${css.gasNval} ${css.col2}"
id="value"
data-id="dandrValue"
value="0"
title="Enter the value and choose the unit"
onkeypress=${(e) => this.validateInputKey(e)}
onchange=${() => this.validateValue()}
>
<select name="unit" class="form-control p-1 ${css.gasNvalUnit} ${css.col2_2} custom-select" id="unit">
<option data-unit="wei">Wei</option>
<option data-unit="gwei">Gwei</option>
<option data-unit="finney">Finney</option>
<option data-unit="ether">Ether</option>
</select>
</div>
</div>
`
const el = yo`
<div class="${css.settings}">
${environmentEl}
${networkEl}
${accountEl}
${gasPriceEl}
${valueEl}
</div>
`
var selectExEnv = environmentEl.querySelector('#selectExEnvOptions')
this.setDropdown(selectExEnv)
this.blockchain.event.register('contextChanged', (context, silent) => {
this.setFinalContext()
})
this.blockchain.event.register('networkStatus', ({ error, network }) => {
if (error) {
this.netUI.innerHTML = 'can\'t detect network '
return
}
const networkProvider = this._components.networkModule.getNetworkProvider.bind(this._components.networkModule)
this.netUI.innerHTML = (networkProvider() !== 'vm') ? `${network.name} (${network.id || '-'}) network` : ''
})
setInterval(() => {
this.fillAccountsList()
}, 1000)
this.el = el
this.fillAccountsList()
return el
}
setDropdown (selectExEnv) {
this.selectExEnv = selectExEnv
const addProvider = (network) => {
selectExEnv.appendChild(yo`<option
title="provider name: ${network.name}"
value="${network.name}"
name="executionContext"
>
${network.name}
</option>`)
addTooltip(yo`<span><b>${network.name}</b> provider added</span>`)
}
const removeProvider = (name) => {
var env = selectExEnv.querySelector(`option[value="${name}"]`)
if (env) {
selectExEnv.removeChild(env)
addTooltip(yo`<span><b>${name}</b> provider removed</span>`)
}
}
this.blockchain.event.register('addProvider', provider => addProvider(provider))
this.blockchain.event.register('removeProvider', name => removeProvider(name))
selectExEnv.addEventListener('change', (event) => {
const provider = selectExEnv.options[selectExEnv.selectedIndex]
const fork = provider.getAttribute('fork') // can be undefined if connected to an external source (web3 provider / injected)
let context = provider.value
context = context.startsWith('vm') ? 'vm' : context // context has to be 'vm', 'web3' or 'injected'
this.setExecutionContext({ context, fork })
})
selectExEnv.value = this._getProviderDropdownValue()
}
setExecutionContext (context) {
this.blockchain.changeExecutionContext(context, () => {
modalDialogCustom.prompt('External node request', this.web3ProviderDialogBody(), 'http://127.0.0.1:8545', (target) => {
this.blockchain.setProviderFromEndpoint(target, context, (alertMsg) => {
if (alertMsg) addTooltip(alertMsg)
this.setFinalContext()
})
}, this.setFinalContext.bind(this))
}, (alertMsg) => {
addTooltip(alertMsg)
}, this.setFinalContext.bind(this))
}
web3ProviderDialogBody () {
const thePath = '<path/to/local/folder/for/test/chain>'
return yo`
<div class="">
Note: To use Geth & https://remix.ethereum.org, configure it to allow requests from Remix:(see <a href="https://geth.ethereum.org/docs/rpc/server" target="_blank">Geth Docs on rpc server</a>)
<div class="border p-1">geth --http --http.corsdomain https://remix.ethereum.org</div>
<br>
To run Remix & a local Geth test node, use this command: (see <a href="https://geth.ethereum.org/getting-started/dev-mode" target="_blank">Geth Docs on Dev mode</a>)
<div class="border p-1">geth --http --http.corsdomain="${window.origin}" --http.api web3,eth,debug,personal,net --vmdebug --datadir ${thePath} --dev console</div>
<br>
<br>
<b>WARNING:</b> It is not safe to use the --http.corsdomain flag with a wildcard: <b>--http.corsdomain *</b>
<br>
<br>For more info: <a href="https://remix-ide.readthedocs.io/en/latest/run.html#more-about-web3-provider" target="_blank">Remix Docs on Web3 Provider</a>
<br>
<br>
Web3 Provider Endpoint
</div>
`
}
/**
* generate a value used by the env dropdown list.
* @return {String} - can return 'vm-berlin, 'vm-london', 'injected' or 'web3'
*/
_getProviderDropdownValue () {
const provider = this.blockchain.getProvider()
const fork = this.blockchain.getCurrentFork()
return provider === 'vm' ? provider + '-' + fork : provider
}
setFinalContext () {
// set the final context. Cause it is possible that this is not the one we've originaly selected
this.selectExEnv.value = this._getProviderDropdownValue()
this.event.trigger('clearInstance', [])
this.updatePlusButton()
}
updatePlusButton () {
// enable/disable + button
const plusBtn = document.getElementById('remixRunPlus')
const plusTitle = document.getElementById('remixRunPlusWraper')
switch (this.selectExEnv.value) {
case 'injected':
plusBtn.classList.add(css.disableMouseEvents)
plusTitle.title = "Unfortunately it's not possible to create an account using injected web3. Please create the account directly from your provider (i.e metamask or other of the same type)."
break
case 'vm':
plusBtn.classList.remove(css.disableMouseEvents)
plusTitle.title = 'Create a new account'
break
case 'web3':
this.onPersonalChange()
break
default: {
plusBtn.classList.add(css.disableMouseEvents)
plusTitle.title = `Unfortunately it's not possible to create an account using an external wallet (${this.selectExEnv.value}).`
}
}
}
onPersonalChange () {
const plusBtn = document.getElementById('remixRunPlus')
const plusTitle = document.getElementById('remixRunPlusWraper')
if (!this._deps.config.get('settings/personal-mode')) {
plusBtn.classList.add(css.disableMouseEvents)
plusTitle.title = 'Creating an account is possible only in Personal mode. Please go to Settings to enable it.'
} else {
plusBtn.classList.remove(css.disableMouseEvents)
plusTitle.title = 'Create a new account'
}
}
newAccount () {
this.blockchain.newAccount(
'',
(cb) => {
modalDialogCustom.promptPassphraseCreation((error, passphrase) => {
if (error) {
return modalDialogCustom.alert(error)
}
cb(passphrase)
}, () => {})
},
(error, address) => {
if (error) {
return addTooltip('Cannot create an account: ' + error)
}
addTooltip(`account ${address} created`)
}
)
}
getSelectedAccount () {
return this.el.querySelector('#txorigin').selectedOptions[0].value
}
getEnvironment () {
return this.blockchain.getProvider()
}
signMessage () {
this.blockchain.getAccounts((err, accounts) => {
if (err) {
return addTooltip(`Cannot get account list: ${err}`)
}
var signMessageDialog = { title: 'Sign a message', text: 'Enter a message to sign', inputvalue: 'Message to sign' }
var $txOrigin = this.el.querySelector('#txorigin')
if (!$txOrigin.selectedOptions[0] && (this.blockchain.isInjectedWeb3() || this.blockchain.isWeb3Provider())) {
return addTooltip('Account list is empty, please make sure the current provider is properly connected to remix')
}
var account = $txOrigin.selectedOptions[0].value
var promptCb = (passphrase) => {
const modal = modalDialogCustom.promptMulti(signMessageDialog, (message) => {
this.blockchain.signMessage(message, account, passphrase, (err, msgHash, signedData) => {
if (err) {
return addTooltip(err)
}
modal.hide()
modalDialogCustom.alert(yo`
<div>
<b>hash:</b><br>
<span id="remixRunSignMsgHash" data-id="settingsRemixRunSignMsgHash">${msgHash}</span>
<br><b>signature:</b><br>
<span id="remixRunSignMsgSignature" data-id="settingsRemixRunSignMsgSignature">${signedData}</span>
</div>
`)
})
}, false)
}
if (this.blockchain.isWeb3Provider()) {
return modalDialogCustom.promptPassphrase(
'Passphrase to sign a message',
'Enter your passphrase for this account to sign the message',
'',
promptCb,
false
)
}
promptCb()
})
}
// TODO: unclear what's the goal of accountListCallId, feels like it can be simplified
async fillAccountsList () {
this.accountListCallId++
const callid = this.accountListCallId
const txOrigin = this.el.querySelector('#txorigin')
let accounts = []
try {
accounts = await this.blockchain.getAccounts()
} catch (e) {
addTooltip(`Cannot get account list: ${e}`)
}
if (!accounts) accounts = []
if (this.accountListCallId > callid) return
this.accountListCallId++
for (const loadedaddress in this.loadedAccounts) {
if (accounts.indexOf(loadedaddress) === -1) {
txOrigin.removeChild(txOrigin.querySelector('option[value="' + loadedaddress + '"]'))
delete this.loadedAccounts[loadedaddress]
}
}
for (const i in accounts) {
const address = accounts[i]
if (!this.loadedAccounts[address]) {
txOrigin.appendChild(yo`<option value="${address}" >${address}</option>`)
this.loadedAccounts[address] = 1
}
}
txOrigin.setAttribute('value', accounts[0])
}
}
module.exports = SettingsUI

@ -3,7 +3,7 @@ import { ViewPlugin } from '@remixproject/engine-web'
import ReactDOM from 'react-dom'
import * as packageJson from '../../../../../package.json'
import { RemixUiSettings } from '@remix-ui/settings' //eslint-disable-line
const globalRegistry = require('../../global/registry')
import Registry from '../state/registry'
const profile = {
name: 'settings',
@ -25,7 +25,7 @@ module.exports = class SettingsTab extends ViewPlugin {
this.config = config
this.editor = editor
this._deps = {
themeModule: globalRegistry.get('themeModule').api
themeModule: Registry.getInstance().get('themeModule').api
}
this.element = document.createElement('div')
this.element.setAttribute('id', 'settingsTab')

@ -1,225 +0,0 @@
var csjs = require('csjs-inject')
var css = csjs`
.runTabView {
display: flex;
flex-direction: column;
}
.runTabView::-webkit-scrollbar {
display: none;
}
.settings {
padding: 0 24px 16px;
}
.crow {
display: block;
margin-top: 8px;
}
.col1 {
width: 30%;
float: left;
align-self: center;
}
.settingsLabel {
font-size: 11px;
margin-bottom: 4px;
text-transform: uppercase;
}
.environment {
display: flex;
align-items: center;
position: relative;
width: 100%;
}
.environment a {
margin-left: 7px;
}
.account {
display: flex;
align-items: center;
}
.account i {
margin-left: 12px;
}
.col2 {
border-radius: 3px;
}
.col2_1 {
width: 164px;
min-width: 164px;
}
.col2_2 {
}
.select {
font-weight: normal;
width: 100%;
overflow: hidden;
}
.instanceContainer {
display: flex;
flex-direction: column;
margin-bottom: 2%;
border: none;
text-align: center;
padding: 0 14px 16px;
}
.pendingTxsContainer {
display: flex;
flex-direction: column;
margin-top: 2%;
border: none;
text-align: center;
}
.container {
padding: 0 24px 16px;
}
.recorderDescription {
margin: 0 15px 15px 0;
}
.contractNames {
width: 100%;
border: 1px solid
}
.subcontainer {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8px;
}
.subcontainer i {
width: 16px;
display: flex;
justify-content: center;
margin-left: 1px;
}
.button button{
flex: none;
}
.button {
display: flex;
align-items: center;
margin-top: 13px;
}
.transaction {
}
.atAddress {
margin: 0;
min-width: 100px;
width: 100px;
height: 100%;
word-break: inherit;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 0;
}
.atAddressSect {
margin-top: 8px;
height: 32px;
}
.atAddressSect input {
height: 32px;
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
.ataddressinput {
padding: .25rem;
}
.create {
}
.input {
font-size: 10px !important;
}
.noInstancesText {
font-style: italic;
text-align: left;
padding-left: 15px;
}
.pendingTxsText {
font-style: italic;
display: flex;
justify-content: space-evenly;
align-items: center;
flex-wrap: wrap;
}
.item {
margin-right: 1em;
display: flex;
align-items: center;
}
.pendingContainer {
display: flex;
align-items: baseline;
}
.pending {
height: 25px;
text-align: center;
padding-left: 10px;
border-radius: 3px;
margin-left: 5px;
}
.disableMouseEvents {
pointer-events: none;
}
.icon {
cursor: pointer;
font-size: 12px;
cursor: pointer;
margin-left: 5px;
}
.icon:hover {
font-size: 12px;
color: var(--warning);
}
.errorIcon {
color: var(--warning);
margin-left: 15px;
}
.failDesc {
color: var(--warning);
padding-left: 10px;
display: inline;
}
.network {
margin-left: 8px;
pointer-events: none;
}
.networkItem {
margin-right: 5px;
}
.transactionActions {
display: flex;
justify-content: space-evenly;
width: 145px;
}
.orLabel {
text-align: center;
text-transform: uppercase;
}
.infoDeployAction {
margin-left: 1px;
font-size: 13px;
color: var(--info);
}
.gasValueContainer {
flex-direction: row;
display: flex;
}
.gasNval {
width: 55%;
font-size: 0.8rem;
}
.gasNvalUnit {
width: 41%;
margin-left: 10px;
font-size: 0.8rem;
}
.deployDropdown {
text-align: center;
text-transform: uppercase;
}
.checkboxAlign {
padding-top: 2px;
}
`
module.exports = css

@ -1,17 +1,14 @@
/* 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 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')
var { UnitTestRunner, assertLibCode } = require('@remix-project/remix-tests')
const profile = {
name: 'solidityUnitTesting',
@ -29,33 +26,17 @@ 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 +51,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 +74,6 @@ module.exports = class TestTab extends ViewPlugin {
}
await this.testRunner.init()
await this.createTestLibs()
this.updateRunAction()
}
onDeactivation () {
@ -104,26 +84,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 +96,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')
await this.call('menuicons', 'select', 'debugger')
setTimeout(async () => { await this.call('debugger', 'debug', txHash, web3) }, 500)
}
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=${async () => 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 +103,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 +128,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,136 +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
}
async 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]
try {
const newFile = await helper.createNonClashingNameAsync(fileNameToImport, this.fileManager, '_test')
if (!await fileProvider.set(newFile, this.generateTestContractSample(hasCurrent, fileName))) { await this.fileManager.open(newFile) }
await this.fileManager.syncEditor(newFile)
} catch (error) {
return modalDialogCustom.alert('Failed to create test file ' + fileNameToImport + ' ' + error)
}
}
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

@ -2,7 +2,7 @@ import { Plugin } from '@remixproject/engine'
import { EventEmitter } from 'events'
import QueryParams from '../../lib/query-params'
import * as packageJson from '../../../../../package.json'
import yo from 'yo-yo'
import Registry from '../state/registry'
const _paq = window._paq = window._paq || []
const themes = [
@ -27,21 +27,25 @@ const profile = {
}
export class ThemeModule extends Plugin {
constructor (registry) {
constructor () {
super(profile)
this.events = new EventEmitter()
this._deps = {
config: registry.get('config').api
config: Registry.getInstance().get('config').api
}
this.themes = themes.reduce((acc, theme) => {
theme.url = window.location.origin + window.location.pathname + theme.url
return { ...acc, [theme.name]: theme }
return { ...acc, [theme.name.toLocaleLowerCase()]: theme }
}, {})
this._paq = _paq
let queryTheme = (new QueryParams()).get().theme
queryTheme = queryTheme && queryTheme.toLocaleLowerCase()
queryTheme = this.themes[queryTheme] ? queryTheme : null
let currentTheme = this._deps.config.get('settings/theme')
currentTheme = currentTheme && currentTheme.toLocaleLowerCase()
currentTheme = this.themes[currentTheme] ? currentTheme : null
this.active = queryTheme || currentTheme || 'Dark'
this.currentThemeState = { queryTheme, currentTheme }
this.active = queryTheme || currentTheme || 'dark'
this.forced = !!queryTheme
}
@ -58,11 +62,16 @@ export class ThemeModule extends Plugin {
/**
* Init the theme
*/
initTheme (callback) {
initTheme (callback) { // callback is setTimeOut in app.js which is always passed
if (callback) this.initCallback = callback
if (this.active) {
const nextTheme = this.themes[this.active] // Theme
document.documentElement.style.setProperty('--theme', nextTheme.quality)
const theme = yo`<link rel="stylesheet" href="${nextTheme.url}" id="theme-link"/>`
const theme = document.createElement('link')
theme.setAttribute('rel', 'stylesheet')
theme.setAttribute('href', nextTheme.url)
theme.setAttribute('id', 'theme-link')
theme.addEventListener('load', () => {
if (callback) callback()
})
@ -75,16 +84,21 @@ export class ThemeModule extends Plugin {
* @param {string} [themeName] - The name of the theme
*/
switchTheme (themeName) {
themeName = themeName && themeName.toLocaleLowerCase()
if (themeName && !Object.keys(this.themes).includes(themeName)) {
throw new Error(`Theme ${themeName} doesn't exist`)
}
const next = themeName || this.active // Name
if (next === this.active) return
if (next === this.active) return // --> exit out of this method
_paq.push(['trackEvent', 'themeModule', 'switchTo', next])
const nextTheme = this.themes[next] // Theme
if (!this.forced) this._deps.config.set('settings/theme', next)
document.getElementById('theme-link').remove()
const theme = yo`<link rel="stylesheet" href="${nextTheme.url}" id="theme-link"/>`
const theme = document.createElement('link')
theme.setAttribute('rel', 'stylesheet')
theme.setAttribute('href', nextTheme.url)
theme.setAttribute('id', 'theme-link')
theme.addEventListener('load', () => {
this.emit('themeLoaded', nextTheme)
this.events.emit('themeLoaded', nextTheme)

@ -1,30 +1,9 @@
var registry = require('../../global/registry')
import Registry from '../state/registry'
var remixLib = require('@remix-project/remix-lib')
var yo = require('yo-yo')
var EventsDecoder = remixLib.execution.EventsDecoder
const transactionDetailsLinks = {
Main: 'https://www.etherscan.io/tx/',
Rinkeby: 'https://rinkeby.etherscan.io/tx/',
Ropsten: 'https://ropsten.etherscan.io/tx/',
Kovan: 'https://kovan.etherscan.io/tx/',
Goerli: 'https://goerli.etherscan.io/tx/'
}
function txDetailsLink (network, hash) {
if (transactionDetailsLinks[network]) {
return transactionDetailsLinks[network] + hash
}
}
export function makeUdapp (blockchain, compilersArtefacts, logHtmlCallback) {
// ----------------- UniversalDApp -----------------
// TODO: to remove when possible
blockchain.event.register('transactionBroadcasted', (txhash, networkName) => {
var txLink = txDetailsLink(networkName, txhash)
if (txLink && logHtmlCallback) logHtmlCallback(yo`<a href="${txLink}" target="_blank">${txLink}</a>`)
})
// ----------------- Tx listener -----------------
const _transactionReceipts = {}
const transactionReceiptResolver = (tx, cb) => {
@ -50,12 +29,12 @@ export function makeUdapp (blockchain, compilersArtefacts, logHtmlCallback) {
}
})
registry.put({ api: txlistener, name: 'txlistener' })
Registry.getInstance().put({ api: txlistener, name: 'txlistener' })
blockchain.startListening(txlistener)
const eventsDecoder = new EventsDecoder({
resolveReceipt: transactionReceiptResolver
})
txlistener.startListening()
registry.put({ api: eventsDecoder, name: 'eventsDecoder' })
Registry.getInstance().put({ api: eventsDecoder, name: 'eventsDecoder' })
}

@ -1,24 +1,13 @@
import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
import { RunTabUI } from '@remix-ui/run-tab'
import { ViewPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json'
const $ = require('jquery')
const yo = require('yo-yo')
const ethJSUtil = require('ethereumjs-util')
const Web3 = require('web3')
const EventManager = require('../../lib/events')
const Card = require('../ui/card')
const css = require('../tabs/styles/run-tab-styles')
const SettingsUI = require('../tabs/runTab/settings.js')
const Recorder = require('../tabs/runTab/model/recorder.js')
const RecorderUI = require('../tabs/runTab/recorder.js')
const DropdownLogic = require('../tabs/runTab/model/dropdownlogic.js')
const ContractDropdownUI = require('../tabs/runTab/contractDropdown.js')
const toaster = require('../ui/tooltip')
const _paq = window._paq = window._paq || []
const UniversalDAppUI = require('../ui/universal-dapp-ui')
const profile = {
name: 'udapp',
displayName: 'Deploy & run transactions',
@ -34,19 +23,25 @@ const profile = {
}
export class RunTab extends ViewPlugin {
constructor (blockchain, config, fileManager, editor, filePanel, compilersArtefacts, networkModule, mainView, fileProvider) {
constructor (blockchain, config, fileManager, editor, filePanel, compilersArtefacts, networkModule, fileProvider) {
super(profile)
this.event = new EventManager()
this.config = config
this.blockchain = blockchain
this.fileManager = fileManager
this.editor = editor
this.logCallback = (msg) => { mainView.getTerminal().logHtml(yo`<pre>${msg}</pre>`) }
this.filePanel = filePanel
this.compilersArtefacts = compilersArtefacts
this.networkModule = networkModule
this.fileProvider = fileProvider
this.recorder = new Recorder(blockchain)
this.REACT_API = {}
this.setupEvents()
this.el = document.createElement('div')
}
onActivation () {
this.renderComponent()
}
setupEvents () {
@ -57,33 +52,19 @@ export class RunTab extends ViewPlugin {
getSettings () {
return new Promise((resolve, reject) => {
if (!this.container) reject(new Error('UI not ready'))
else {
resolve({
selectedAccount: this.settingsUI.getSelectedAccount(),
selectedEnvMode: this.blockchain.getProvider(),
networkEnvironment: this.container.querySelector('*[data-id="settingsNetworkEnv"]').textContent
}
)
}
resolve({
selectedAccount: this.REACT_API.accounts.selectedAccount,
selectedEnvMode: this.REACT_API.selectExEnv,
networkEnvironment: this.REACT_API.networkName
})
})
}
async setEnvironmentMode (env) {
const canCall = await this.askUserPermission('setEnvironmentMode', 'change the environment used')
if (canCall) {
toaster(yo`
<div>
<i class="fas fa-exclamation-triangle text-danger mr-1"></i>
<span>
${this.currentRequest.from}
<span class="font-weight-bold text-warning">
is changing your environment to
</span> ${env}
</span>
</div>
`, '', { time: 3000 })
this.settingsUI.setExecutionContext(env)
env = typeof env === 'string' ? { context: env } : env
this.emit('setEnvironmentModeReducer', env, this.currentRequest.from)
}
}
@ -104,183 +85,25 @@ export class RunTab extends ViewPlugin {
return this.blockchain.pendingTransactionsCount()
}
renderContainer () {
this.container = yo`<div class="${css.runTabView} run-tab" id="runTabView" data-id="runTabView"></div>`
var el = yo`
<div class="list-group list-group-flush">
${this.settingsUI.render()}
${this.contractDropdownUI.render()}
${this.recorderCard.render()}
${this.instanceContainer}
</div>
`
this.container.appendChild(el)
return this.container
}
renderInstanceContainer () {
this.instanceContainer = yo`<div class="${css.instanceContainer} border-0 list-group-item"></div>`
const instanceContainerTitle = yo`
<div class="d-flex justify-content-between align-items-center pl-2 ml-1 mb-2"
title="Autogenerated generic user interfaces for interaction with deployed contracts">
Deployed Contracts
<i class="mr-2 ${css.icon} far fa-trash-alt" data-id="deployAndRunClearInstances" onclick=${() => this.event.trigger('clearInstance', [])}
title="Clear instances list and reset recorder" aria-hidden="true">
</i>
</div>`
this.noInstancesText = yo`
<span class="mx-2 mt-3 alert alert-warning" data-id="deployAndRunNoInstanceText" role="alert">
Currently you have no contract instances to interact with.
</span>`
this.event.register('clearInstance', () => {
this.instanceContainer.innerHTML = '' // clear the instances list
this.instanceContainer.appendChild(instanceContainerTitle)
this.instanceContainer.appendChild(this.noInstancesText)
})
this.instanceContainer.appendChild(instanceContainerTitle)
this.instanceContainer.appendChild(this.noInstancesText)
}
renderSettings () {
this.settingsUI = new SettingsUI(this.blockchain, this.networkModule)
this.settingsUI.event.register('clearInstance', () => {
this.event.trigger('clearInstance', [])
})
render () {
return this.el
}
renderDropdown (udappUI, fileManager, compilersArtefacts, config, editor, logCallback) {
const dropdownLogic = new DropdownLogic(compilersArtefacts, config, editor, this)
this.contractDropdownUI = new ContractDropdownUI(this.blockchain, dropdownLogic, logCallback, this)
fileManager.events.on('currentFileChanged', this.contractDropdownUI.changeCurrentFile.bind(this.contractDropdownUI))
this.contractDropdownUI.event.register('clearInstance', () => {
const noInstancesText = this.noInstancesText
if (noInstancesText.parentNode) { noInstancesText.parentNode.removeChild(noInstancesText) }
})
this.contractDropdownUI.event.register('newContractABIAdded', (abi, address) => {
this.instanceContainer.appendChild(udappUI.renderInstanceFromABI(abi, address, '<at address>'))
})
this.contractDropdownUI.event.register('newContractInstanceAdded', (contractObject, address, value) => {
this.instanceContainer.appendChild(udappUI.renderInstance(contractObject, address, value))
})
renderComponent () {
ReactDOM.render(
<RunTabUI plugin={this} />
, this.el)
}
renderRecorder (udappUI, fileManager, config, logCallback) {
this.recorderCount = yo`<span>0</span>`
const recorder = new Recorder(this.blockchain)
recorder.event.register('recorderCountChange', (count) => {
this.recorderCount.innerText = count
})
this.event.register('clearInstance', recorder.clearAll.bind(recorder))
this.recorderInterface = new RecorderUI(this.blockchain, fileManager, recorder, logCallback, config)
this.recorderInterface.event.register('newScenario', (abi, address, contractName) => {
var noInstancesText = this.noInstancesText
if (noInstancesText.parentNode) { noInstancesText.parentNode.removeChild(noInstancesText) }
this.instanceContainer.appendChild(udappUI.renderInstanceFromABI(abi, address, contractName))
})
this.recorderInterface.render()
onReady (api) {
this.REACT_API = api
}
renderRecorderCard () {
const collapsedView = yo`
<div class="d-flex flex-column">
<div class="ml-2 badge badge-pill badge-primary" title="The number of recorded transactions">${this.recorderCount}</div>
</div>`
const expandedView = yo`
<div class="d-flex flex-column">
<div class="${css.recorderDescription} mt-2">
All transactions (deployed contracts and function executions) in this environment can be saved and replayed in
another environment. e.g Transactions created in Javascript VM can be replayed in the Injected Web3.
</div>
<div class="${css.transactionActions}">
${this.recorderInterface.recordButton}
${this.recorderInterface.runButton}
</div>
</div>
</div>`
this.recorderCard = new Card({}, {}, { title: 'Transactions recorded', collapsedView: collapsedView })
this.recorderCard.event.register('expandCollapseCard', (arrow, body, status) => {
body.innerHTML = ''
status.innerHTML = ''
if (arrow === 'down') {
status.appendChild(collapsedView)
body.appendChild(expandedView)
} else if (arrow === 'up') {
status.appendChild(collapsedView)
}
})
writeFile (fileName, content) {
return this.call('fileManager', 'writeFile', fileName, content)
}
render () {
this.udappUI = new UniversalDAppUI(this.blockchain, this.logCallback)
this.blockchain.resetAndInit(this.config, {
getAddress: (cb) => {
cb(null, $('#txorigin').val())
},
getValue: (cb) => {
try {
const number = document.querySelector('#value').value
const select = document.getElementById('unit')
const index = select.selectedIndex
const selectedUnit = select.querySelectorAll('option')[index].dataset.unit
let unit = 'ether' // default
if (['ether', 'finney', 'gwei', 'wei'].indexOf(selectedUnit) >= 0) {
unit = selectedUnit
}
cb(null, Web3.utils.toWei(number, unit))
} catch (e) {
cb(e)
}
},
getGasLimit: (cb) => {
try {
cb(null, '0x' + new ethJSUtil.BN($('#gasLimit').val(), 10).toString(16))
} catch (e) {
cb(e.message)
}
}
})
this.renderInstanceContainer()
this.renderSettings()
this.renderDropdown(this.udappUI, this.fileManager, this.compilersArtefacts, this.config, this.editor, this.logCallback)
this.renderRecorder(this.udappUI, this.fileManager, this.config, this.logCallback)
this.renderRecorderCard()
const addPluginProvider = (profile) => {
if (profile.kind === 'provider') {
((profile, app) => {
const web3Provider = {
async sendAsync (payload, callback) {
try {
const result = await app.call(profile.name, 'sendAsync', payload)
callback(null, result)
} catch (e) {
callback(e)
}
}
}
app.blockchain.addProvider({ name: profile.displayName, provider: web3Provider })
})(profile, this)
}
}
const removePluginProvider = (profile) => {
if (profile.kind === 'provider') this.blockchain.removeProvider(profile.displayName)
}
this.on('manager', 'pluginActivated', addPluginProvider.bind(this))
this.on('manager', 'pluginDeactivated', removePluginProvider.bind(this))
return this.renderContainer()
readFile (fileName) {
return this.call('fileManager', 'readFile', fileName)
}
}

@ -1,212 +0,0 @@
'use strict'
var yo = require('yo-yo')
var csjs = require('csjs-inject')
var css = csjs`
.li_tv {
list-style-type: none;
-webkit-margin-before: 0px;
-webkit-margin-after: 0px;
-webkit-margin-start: 0px;
-webkit-margin-end: 0px;
-webkit-padding-start: 0px;
}
.ul_tv {
list-style-type: none;
-webkit-margin-before: 0px;
-webkit-margin-after: 0px;
-webkit-margin-start: 0px;
-webkit-margin-end: 0px;
-webkit-padding-start: 0px;
}
.caret_tv {
width: 10px;
flex-shrink: 0;
padding-right: 5px;
}
.label_item {
word-break: break-all;
}
.label_key {
min-width: max-content;
max-width: 80%;
word-break: break-word;
}
.label_value {
min-width: 10%;
}
.cursor_pointer {
cursor: pointer;
}
`
var EventManager = require('../../lib/events')
/**
* TreeView
* - extendable by specifying custom `extractData` and `formatSelf` function
* - trigger `nodeClick` and `leafClick`
*/
class TreeView {
constructor (opts) {
this.event = new EventManager()
this.extractData = opts.extractData || this.extractDataDefault
this.formatSelf = opts.formatSelf || this.formatSelfDefault
this.loadMore = opts.loadMore
this.view = null
this.expandPath = []
}
render (json, expand) {
var view = this.renderProperties(json, expand)
if (!this.view) {
this.view = view
}
return view
}
update (json) {
if (this.view) {
yo.update(this.view, this.render(json))
}
}
renderObject (item, parent, key, expand, keyPath) {
var data = this.extractData(item, parent, key)
var children = (data.children || []).map((child, index) => {
return this.renderObject(child.value, data, child.key, expand, keyPath + '/' + child.key)
})
return this.formatData(key, data, children, expand, keyPath)
}
renderProperties (json, expand, key) {
key = key || ''
var children = Object.keys(json).map((innerkey) => {
return this.renderObject(json[innerkey], json, innerkey, expand, innerkey)
})
return yo`<ul key=${key} data-id="treeViewUl${key}" class="${css.ul_tv} ml-0 px-2">${children}</ul>`
}
formatData (key, data, children, expand, keyPath) {
var self = this
var li = yo`<li key=${keyPath} data-id="treeViewLi${keyPath}" class=${css.li_tv}></li>`
var caret = yo`<div class="px-1 fas fa-caret-right caret ${css.caret_tv}"></div>`
var label = yo`
<div key=${keyPath} data-id="treeViewDiv${keyPath}" class="d-flex flex-row align-items-center">
${caret}
<span class="w-100">${self.formatSelf(key, data, li)}</span>
</div>`
const expanded = self.expandPath.includes(keyPath)
li.appendChild(label)
if (data.children) {
var list = yo`<ul key=${keyPath} data-id="treeViewUlList${keyPath}" class="pl-2 ${css.ul_tv}">${children}</ul>`
list.style.display = expanded ? 'block' : 'none'
caret.className = list.style.display === 'none' ? `fas fa-caret-right caret ${css.caret_tv}` : `fas fa-caret-down caret ${css.caret_tv}`
caret.setAttribute('data-id', `treeViewToggle${keyPath}`)
label.onclick = function () {
self.expand(keyPath)
if (self.isExpanded(keyPath)) {
if (!self.expandPath.includes(keyPath)) self.expandPath.push(keyPath)
} else {
self.expandPath = self.expandPath.filter(path => !path.startsWith(keyPath))
}
}
label.oncontextmenu = function (event) {
self.event.trigger('nodeRightClick', [keyPath, data, label, event])
}
li.appendChild(list)
if (data.hasNext) {
list.appendChild(yo`<li><span class="w-100 text-primary ${css.cursor_pointer}" data-id="treeViewLoadMore" onclick="${() => self.loadMore(data.cursor)}">Load more</span></li>`)
}
} else {
caret.style.visibility = 'hidden'
label.oncontextmenu = function (event) {
self.event.trigger('leafRightClick', [keyPath, data, label, event])
}
label.onclick = function (event) {
self.event.trigger('leafClick', [keyPath, data, label, event])
}
}
return li
}
isExpanded (path) {
var current = this.nodeAt(path)
if (current) {
return current.style.display !== 'none'
}
return false
}
expand (path) {
var caret = this.caretAt(path)
var node = this.nodeAt(path)
if (node) {
node.style.display = node.style.display === 'none' ? 'block' : 'none'
caret.className = node.style.display === 'none' ? `fas fa-caret-right caret ${css.caret_tv}` : `fas fa-caret-down caret ${css.caret_tv}`
this.event.trigger('nodeClick', [path, node])
}
}
caretAt (path) {
var label = this.labelAt(path)
if (label) {
return label.querySelector('.caret')
}
}
itemAt (path) {
return this.view.querySelector(`li[key="${path}"]`)
}
labelAt (path) {
return this.view.querySelector(`div[key="${path}"]`)
}
nodeAt (path) {
return this.view.querySelector(`ul[key="${path}"]`)
}
updateNodeFromJSON (path, jsonTree, expand) {
var newTree = this.renderProperties(jsonTree, expand, path)
var current = this.nodeAt(path)
if (current && current.parentElement) {
current.parentElement.replaceChild(newTree, current)
}
}
formatSelfDefault (key, data) {
return yo`
<div class="d-flex mt-2 flex-row ${css.label_item}">
<label class="small font-weight-bold pr-1 ${css.label_key}">${key}:</label>
<label class="m-0 ${css.label_value}">${data.self}</label>
</div>
`
}
extractDataDefault (item, parent, key) {
var ret = {}
if (item instanceof Array) {
ret.children = item.map((item, index) => {
return { key: index, value: item }
})
ret.self = 'Array'
ret.isNode = true
ret.isLeaf = false
} else if (item instanceof Object) {
ret.children = Object.keys(item).map((key) => {
return { key: key, value: item[key] }
})
ret.self = 'Object'
ret.isNode = true
ret.isLeaf = false
} else {
ret.self = item
ret.children = null
ret.isNode = false
ret.isLeaf = true
}
return ret
}
}
module.exports = TreeView

@ -1,212 +0,0 @@
var yo = require('yo-yo')
var remixLib = require('@remix-project/remix-lib')
var EventManager = remixLib.EventManager
var Commands = require('../../lib/commands')
// -------------- styling ----------------------
var css = require('./styles/auto-complete-popup-styles')
/* USAGE:
var autoCompletePopup = new AutoCompletePopup({
options: []
})
autoCompletePopup.event.register('handleSelect', function (input) { })
autoCompletePopup.event.register('updateList', function () { })
*/
class AutoCompletePopup {
constructor (opts = {}) {
var self = this
self.event = new EventManager()
self.isOpen = false
self.opts = opts
self.data = {
_options: []
}
self._components = {}
self._view = null
self._startingElement = 0
self._elementsToShow = 4
self._selectedElement = 0
this.extraCommands = []
}
render () {
var self = this
const autoComplete = yo`
<div class="${css.popup} alert alert-secondary">
<div>
${self.data._options.map((item, index) => {
return yo`
<div data-id="autoCompletePopUpAutoCompleteItem" class="${css.autoCompleteItem} ${css.listHandlerHide} item ${self._selectedElement === index ? 'border border-primary' : ''}">
<div value=${index} onclick=${(event) => { self.handleSelect(event.srcElement.innerText) }}>
${getKeyOf(item)}
</div>
<div>
${getValueOf(item)}
</div>
</div>
`
})}
</div>
<div class="${css.listHandlerHide}">
<div class="${css.pageNumberAlignment}">Page ${(self._startingElement / self._elementsToShow) + 1} of ${Math.ceil(self.data._options.length / self._elementsToShow)}</div>
</div>
</div>
`
function setUpPopUp (autoComplete) {
handleOpenPopup(autoComplete)
handleListSize(autoComplete)
}
function handleOpenPopup (autoComplete) {
autoComplete.style.display = self.data._options.length > 0 ? 'block' : 'none'
}
function handleListSize (autoComplete) {
if (self.data._options.length >= self._startingElement) {
for (let i = self._startingElement; i < (self._elementsToShow + self._startingElement); i++) {
const el = autoComplete.querySelectorAll('.item')[i]
if (el) {
el.classList.remove(css.listHandlerHide)
el.classList.add(css.listHandlerShow)
}
}
}
}
setUpPopUp(autoComplete)
if (!this._view) this._view = autoComplete
return autoComplete
}
handleSelect (text) {
this.removeAutoComplete()
this.event.trigger('handleSelect', [text])
}
moveUp () {
if (this._selectedElement === 0) return
this._selectedElement--
this._startingElement = this._selectedElement > 0 ? this._selectedElement - 1 : 0
this.event.trigger('updateList')
yo.update(this._view, this.render())
}
moveDown () {
if (this.data._options.length <= this._selectedElement + 1) return
this._selectedElement++
this._startingElement = this._selectedElement - 1
this.event.trigger('updateList')
yo.update(this._view, this.render())
}
handleAutoComplete (event, inputString) {
if (this.isOpen && (event.which === 27 || event.which === 8 || event.which === 46)) {
// backspace or any key that should remove the autocompletion
this.removeAutoComplete()
return true
}
if (this.isOpen && (event.which === 13 || event.which === 9)) {
// enter and tab (validate completion)
event.preventDefault()
if (this.data._options[this._selectedElement]) {
this.handleSelect(getKeyOf(this.data._options[this._selectedElement]))
}
this.removeAutoComplete()
return true
}
if (this.isOpen && event.which === 38) {
// move up
event.preventDefault()
this.isOpen = true
this.moveUp()
return true
}
if (this.isOpen && event.which === 40) {
// move down
event.preventDefault()
this.isOpen = true
this.moveDown()
return true
}
if (event.which === 13 || event.which === 9) {
// enter || tab and autocompletion is off, just returning false
return false
}
const textList = inputString.split(' ')
const autoCompleteInput = textList.length > 1 ? textList[textList.length - 1] : textList[0]
if (inputString.length >= 2) {
// more than 2 letters, start completion
this.data._options = []
Commands.allPrograms.forEach(item => {
const program = getKeyOf(item)
if (program.substring(0, program.length - 1).includes(autoCompleteInput.trim())) {
this.data._options.push(item)
} else if (autoCompleteInput.trim().includes(program) || (program === autoCompleteInput.trim())) {
Commands.allCommands.forEach(item => {
const command = getKeyOf(item)
if (command.includes(autoCompleteInput.trim())) {
this.data._options.push(item)
}
})
}
})
this.extraCommands.forEach(item => {
const command = getKeyOf(item)
if (command.includes(autoCompleteInput.trim())) {
this.data._options.push(item)
}
})
if (this.data._options.length === 1 && event.which === 9) {
// if only one option and tab is pressed, we resolve it
event.preventDefault()
textList.pop()
textList.push(getKeyOf(this.data._options[0]))
this.handleSelect(`${textList}`.replace(/,/g, ' '))
this.removeAutoComplete()
return
}
if (this.data._options.length) this.isOpen = true
yo.update(this._view, this.render())
return true
}
return false
}
removeAutoComplete () {
if (!this.isOpen) return
this._view.style.display = 'none'
this.isOpen = false
this.data._options = []
this._startingElement = 0
this._selectedElement = 0
yo.update(this._view, this.render())
}
extendAutocompletion () {
// TODO: this is not using the appManager interface. Terminal should be put as module
this.opts.appManager.event.on('activate', async (profile) => {
if (!profile.methods) return
profile.methods.forEach((method) => {
const key = `remix.call('${profile.name}', '${method}')`
const keyValue = {}
keyValue[key] = `call ${profile.name} - ${method}`
if (this.extraCommands.includes(keyValue)) return
this.extraCommands.push(keyValue)
})
})
}
}
function getKeyOf (item) {
return Object.keys(item)[0]
}
function getValueOf (item) {
return Object.values(item)[0]
}
module.exports = AutoCompletePopup

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save