rebase from upstream master

pull/1527/head
Andrew Miracle 3 years ago
commit c988ce9c07
  1. 19
      .circleci/config.yml
  2. 3
      .gitignore
  3. 82
      README.md
  4. 11
      apps/debugger/src/app/debugger-api.ts
  5. 30
      apps/remix-ide-e2e/src/commands/acceptAndRemember.ts
  6. 29
      apps/remix-ide-e2e/src/commands/addFile.ts
  7. 46
      apps/remix-ide-e2e/src/commands/addLocalPlugin.ts
  8. 2
      apps/remix-ide-e2e/src/commands/checkAnnotations.ts
  9. 2
      apps/remix-ide-e2e/src/commands/checkAnnotationsNotPresent.ts
  10. 22
      apps/remix-ide-e2e/src/commands/editorScroll.ts
  11. 1
      apps/remix-ide-e2e/src/commands/executeScript.ts
  12. 4
      apps/remix-ide-e2e/src/commands/getEditorValue.ts
  13. 7
      apps/remix-ide-e2e/src/commands/journalLastChildIncludes.ts
  14. 4
      apps/remix-ide-e2e/src/commands/removeFile.ts
  15. 17
      apps/remix-ide-e2e/src/commands/scrollToLine.ts
  16. 4
      apps/remix-ide-e2e/src/commands/setEditorValue.ts
  17. 3
      apps/remix-ide-e2e/src/commands/testFunction.ts
  18. 1
      apps/remix-ide-e2e/src/commands/verifyContracts.ts
  19. 13
      apps/remix-ide-e2e/src/local-plugin/src/app/Client.ts
  20. 129
      apps/remix-ide-e2e/src/local-plugin/src/app/app.css
  21. 131
      apps/remix-ide-e2e/src/local-plugin/src/app/app.tsx
  22. 9
      apps/remix-ide-e2e/src/local-plugin/src/app/logger.tsx
  23. 17
      apps/remix-ide-e2e/src/local-plugin/src/app/logo.svg
  24. 11
      apps/remix-ide-e2e/src/local-plugin/src/app/star.svg
  25. 5
      apps/remix-ide-e2e/src/local-plugin/src/index.html
  26. 18
      apps/remix-ide-e2e/src/tests/ballot.test.ts
  27. 5
      apps/remix-ide-e2e/src/tests/ballot_0_4_11.spec.ts
  28. 2
      apps/remix-ide-e2e/src/tests/compiler_api.test.ts
  29. 15
      apps/remix-ide-e2e/src/tests/debugger.spec.ts
  30. 7
      apps/remix-ide-e2e/src/tests/defaultLayout.test.ts
  31. 122
      apps/remix-ide-e2e/src/tests/editor.spec.ts
  32. 13
      apps/remix-ide-e2e/src/tests/fileExplorer.test.ts
  33. 12
      apps/remix-ide-e2e/src/tests/fileManager_api.spec.ts
  34. 16
      apps/remix-ide-e2e/src/tests/gist.spec.ts
  35. 1
      apps/remix-ide-e2e/src/tests/libraryDeployment.test.ts
  36. 78
      apps/remix-ide-e2e/src/tests/pluginManager.test.ts
  37. 327
      apps/remix-ide-e2e/src/tests/plugin_api.ts
  38. 14
      apps/remix-ide-e2e/src/tests/recorder.spec.ts
  39. 3
      apps/remix-ide-e2e/src/tests/remixd.test.ts
  40. 4
      apps/remix-ide-e2e/src/tests/solidityImport.spec.ts
  41. 112
      apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts
  42. 37
      apps/remix-ide-e2e/src/tests/terminal.test.ts
  43. 15
      apps/remix-ide-e2e/src/tests/transactionExecution.spec.ts
  44. 24
      apps/remix-ide-e2e/src/tests/workspace.test.ts
  45. 8
      apps/remix-ide-e2e/src/types/index.d.ts
  46. 2
      apps/remix-ide/ci/browser_tests_plugin_api.sh
  47. 596
      apps/remix-ide/src/app.js
  48. 2
      apps/remix-ide/src/app/components/main-panel.js
  49. 84
      apps/remix-ide/src/app/editor/SourceHighlighters.js
  50. 16
      apps/remix-ide/src/app/editor/contextualListener.js
  51. 530
      apps/remix-ide/src/app/editor/editor.js
  52. 86
      apps/remix-ide/src/app/editor/sourceHighlighter.js
  53. 4
      apps/remix-ide/src/app/files/dgitProvider.js
  54. 10
      apps/remix-ide/src/app/files/fileManager.js
  55. 25
      apps/remix-ide/src/app/files/remixDProvider.js
  56. 24
      apps/remix-ide/src/app/files/workspaceFileProvider.js
  57. 274
      apps/remix-ide/src/app/panels/file-panel.js
  58. 2
      apps/remix-ide/src/app/panels/main-view.js
  59. 194
      apps/remix-ide/src/app/panels/tab-proxy.js
  60. 830
      apps/remix-ide/src/app/panels/terminal.js
  61. 6
      apps/remix-ide/src/app/tabs/analysis-tab.js
  62. 24
      apps/remix-ide/src/app/tabs/compile-tab.js
  63. 9
      apps/remix-ide/src/app/tabs/runTab/contractDropdown.js
  64. 8
      apps/remix-ide/src/app/tabs/runTab/settings.js
  65. 54
      apps/remix-ide/src/app/tabs/test-tab.js
  66. 2
      apps/remix-ide/src/app/tabs/theme-module.js
  67. 1
      apps/remix-ide/src/assets/css/intro.js/2.7.0/introjs.min.css
  68. 2
      apps/remix-ide/src/assets/css/intro.js/4.1.0/introjs.min.css
  69. 8816
      apps/remix-ide/src/assets/css/themes/remix-night_owl.css
  70. 1
      apps/remix-ide/src/blockchain/execution-context.js
  71. 1
      apps/remix-ide/src/blockchain/providers/vm.js
  72. 3
      apps/remix-ide/src/lib/cmdInterpreterAPI.js
  73. 2
      apps/remix-ide/src/remixAppManager.js
  74. 1
      apps/remix-ide/src/remixEngine.js
  75. 1
      apps/solidity-compiler/src/app/compiler.ts
  76. 3
      libs/remix-analyzer/.eslintrc
  77. 14
      libs/remix-analyzer/package.json
  78. 3
      libs/remix-astwalker/.eslintrc
  79. 12
      libs/remix-astwalker/package.json
  80. 1
      libs/remix-astwalker/src/astWalker.ts
  81. 1
      libs/remix-astwalker/src/sourceMappings.ts
  82. 2
      libs/remix-core-plugin/src/lib/compiler-artefacts.ts
  83. 23
      libs/remix-debug/package.json
  84. 3
      libs/remix-lib/.eslintrc
  85. 11
      libs/remix-lib/package.json
  86. 12
      libs/remix-lib/src/web3Provider/web3VmProvider.ts
  87. 14
      libs/remix-simulator/package.json
  88. 28
      libs/remix-simulator/test/blocks.ts
  89. 3
      libs/remix-solidity/.eslintrc
  90. 12
      libs/remix-solidity/package.json
  91. 2
      libs/remix-solidity/src/compiler/types.ts
  92. 3
      libs/remix-tests/.eslintrc
  93. 2
      libs/remix-tests/jest.config.js
  94. 18
      libs/remix-tests/package.json
  95. 3
      libs/remix-tests/src/compiler.ts
  96. 5
      libs/remix-tests/src/deployer.ts
  97. 4
      libs/remix-tests/src/run.ts
  98. 4
      libs/remix-tests/src/runTestFiles.ts
  99. 8
      libs/remix-tests/src/runTestSources.ts
  100. 32
      libs/remix-tests/src/testRunner.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -51,6 +51,7 @@ jobs:
- checkout - checkout
- run: npm install - run: npm install
- run: npm run build:libs - run: npm run build:libs
- run: cd dist/libs/remix-tests && npm install
- run: npm run test:libs - run: npm run test:libs
remix-ide-chrome-1: remix-ide-chrome-1:
@ -220,7 +221,7 @@ jobs:
- store_artifacts: - store_artifacts:
path: ./reports/screenshots path: ./reports/screenshots
remix-ide-plugin-manager: remix-ide-plugin-api:
docker: docker:
# specify the version you desire here # specify the version you desire here
- image: circleci/node:14.17.6-browsers - image: circleci/node:14.17.6-browsers
@ -248,13 +249,11 @@ jobs:
name: Start Selenium name: Start Selenium
command: ./node_modules/.bin/selenium-standalone start --drivers.chrome.version=2.39 --drivers.chrome.baseURL=https://chromedriver.storage.googleapis.com command: ./node_modules/.bin/selenium-standalone start --drivers.chrome.version=2.39 --drivers.chrome.baseURL=https://chromedriver.storage.googleapis.com
background: true background: true
- run: ./apps/remix-ide/ci/browser_tests_plugin_manager.sh - run: ./apps/remix-ide/ci/browser_tests_plugin_api.sh
- store_test_results: - store_test_results:
path: ./reports/tests path: ./reports/tests
- store_artifacts: - store_artifacts:
path: ./reports/screenshots path: ./reports/screenshots
deploy-remix-live: deploy-remix-live:
docker: docker:
# specify the version you desire here # specify the version you desire here
@ -374,6 +373,9 @@ workflows:
- remix-libs: - remix-libs:
requires: requires:
- lint - lint
- remix-ide-plugin-api:
requires:
- lint
- remix-ide-chrome-1: - remix-ide-chrome-1:
requires: requires:
- lint - lint
@ -389,9 +391,6 @@ workflows:
- remix-ide-run-deploy: - remix-ide-run-deploy:
requires: requires:
- lint - lint
- remix-ide-plugin-manager:
requires:
- lint
- publish: - publish:
requires: requires:
- lint - lint
@ -402,7 +401,7 @@ workflows:
- remix-ide-firefox-1 - remix-ide-firefox-1
- remix-ide-firefox-2 - remix-ide-firefox-2
- remix-ide-run-deploy - remix-ide-run-deploy
- remix-ide-plugin-manager - remix-ide-plugin-api
filters: filters:
branches: branches:
only: remix_live only: remix_live
@ -413,7 +412,7 @@ workflows:
- remix-ide-firefox-1 - remix-ide-firefox-1
- remix-ide-firefox-2 - remix-ide-firefox-2
- remix-ide-run-deploy - remix-ide-run-deploy
- remix-ide-plugin-manager - remix-ide-plugin-api
filters: filters:
branches: branches:
only: master only: master
@ -424,7 +423,7 @@ workflows:
- remix-ide-firefox-1 - remix-ide-firefox-1
- remix-ide-firefox-2 - remix-ide-firefox-2
- remix-ide-run-deploy - remix-ide-run-deploy
- remix-ide-plugin-manager - remix-ide-plugin-api
filters: filters:
branches: branches:
only: remix_beta only: remix_beta

3
.gitignore vendored

@ -30,6 +30,7 @@ soljson.js
*.launch *.launch
.settings/ .settings/
*.sublime-workspace *.sublime-workspace
.vscode/
# IDE - VSCode # IDE - VSCode
.vscode/* .vscode/*
@ -51,3 +52,5 @@ testem.log
# System Files # System Files
.DS_Store .DS_Store
.vscode/settings.json
.vscode/launch.json

@ -1,66 +1,75 @@
[![Join the chat at https://gitter.im/ethereum/remix](https://badges.gitter.im/ethereum/remix.svg)](https://gitter.im/ethereum/remix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![CircleCI](https://circleci.com/gh/ethereum/remix-project.svg?style=svg)](https://circleci.com/gh/ethereum/remix-project) [![CircleCI](https://circleci.com/gh/ethereum/remix-project.svg?style=svg)](https://circleci.com/gh/ethereum/remix-project)
[![Documentation Status](https://readthedocs.org/projects/docs/badge/?version=latest)](https://remix-ide.readthedocs.io/en/latest/index.html) [![Documentation Status](https://readthedocs.org/projects/docs/badge/?version=latest)](https://remix-ide.readthedocs.io/en/latest/index.html)
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/ethereum/remix-project/blob/master/CONTRIBUTING.md)
![GitHub contributors](https://img.shields.io/github/contributors/ethereum/remix-project)
[![Awesome Remix](https://img.shields.io/badge/Awesome--Remix-resources-green)](https://github.com/ethereum/awesome-remix)
![GitHub](https://img.shields.io/github/license/ethereum/remix-project)
[![Join the chat at https://gitter.im/ethereum/remix](https://badges.gitter.im/ethereum/remix.svg)](https://gitter.im/ethereum/remix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Twitter Follow](https://img.shields.io/twitter/follow/ethereumremix?style=social)](https://twitter.com/ethereumremix)
# Remix Project # Remix Project
**Remix Project** is a platform for development tools that use a plugin architecture. It encompasses sub-projects including Remix Plugin Engine, Remix Libraries, and of course Remix IDE. **Remix Project** is a platform for development tools that use a plugin architecture. It encompasses sub-projects including Remix Plugin Engine, Remix Libraries, and of course Remix IDE.
**Remix IDE** is an open source web and desktop application. It fosters a fast development cycle and has a rich set of plugins with intuitive GUIs. Remix is used for the **entire journey of contract development with [Solidity language](https://soliditylang.org/)** in as well as being a playground for learning and teaching Ethereum. **Remix IDE** is an open source web and desktop application. It fosters a fast development cycle and has a rich set of plugins with intuitive GUIs. Remix is used for the **entire journey of contract development with [Solidity language](https://soliditylang.org/)** as well as a playground for learning and teaching [Ethereum](https://ethereum.org/).
To try web app, visit: [https://remix.ethereum.org](https://remix.ethereum.org). Start developing using Remix on browser, visit: [https://remix.ethereum.org](https://remix.ethereum.org)
For desktop version, See releases: [https://github.com/ethereum/remix-desktop/releases](https://github.com/ethereum/remix-desktop/releases) For desktop version, see releases: [https://github.com/ethereum/remix-desktop/releases](https://github.com/ethereum/remix-desktop/releases)
![Remix screenshot](https://github.com/ethereum/remix-project/raw/master/apps/remix-ide/remix_screenshot.png) ![Remix screenshot](https://github.com/ethereum/remix-project/raw/master/apps/remix-ide/remix_screenshot.png)
**Remix libraries** work as a core of native plugins of Remix IDE. Read more about libraries [here](libs/README.md) :point_right: **Remix libraries** work as a core of native plugins of Remix IDE. Read more about libraries [here](libs/README.md)
## Offline Usage ## Offline Usage
The `master` branch has always the latest stable build of Remix. It also contains a ZIP file with the entire build. Download it to use offline. The `gh-pages` branch of [remix-live](https://github.com/ethereum/remix-live) always has the latest stable build of Remix. It contains a ZIP file with the entire build. Download it to use offline.
Note: It contains the latest release of Solidity available at the time of the packaging. No other compiler versions are supported. Note: It contains the latest supported version of Solidity available at the time of the packaging. Other compiler versions can be used online only.
## Setup ## Setup
Install **npm** and **node.js** (see https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), then * Install **NPM** and **Node.js**. See [Guide](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) <br/>
install [Nx CLI](https://nx.dev/react/cli/overview) globally to enable running **nx executable commands**. *Supported versions:*
```bash
"engines": {
"node": "^14.17.6",
"npm": "^6.14.15"
}
```
* Install [Nx CLI](https://nx.dev/react/cli/overview) globally to enable running **nx executable commands**.
```bash ```bash
npm install -g @nrwl/cli npm install -g @nrwl/cli
``` ```
* Clone the github repository (`wget` need to be installed first):
Clone the github repository (`wget` need to be installed first):
```bash ```bash
git clone https://github.com/ethereum/remix-project.git git clone https://github.com/ethereum/remix-project.git
``` ```
* Build `remix-project`:
And build it:
```bash ```bash
cd remix-project cd remix-project
npm install npm install
nx build remix-ide --with-deps nx build
nx serve nx serve
``` ```
Run `nx serve` and open `http://127.0.0.1:8080` in your browser. Open `http://127.0.0.1:8080` in your browser to load Remix IDE locally.
Then open your `text editor` and start developing. Go to your `text editor` and start developing. Browser will automatically refresh when files are saved.
The browser will automatically refresh when files are saved.
## Production Build ## Production Build
To generate react production builds for remix-project. To generate react production builds for remix-project.
```bash ```bash
npm run build:production npm run build:production
``` ```
build can be found in `remix-project/dist/apps/remix-ide` directory. Build can be found in `remix-project/dist/apps/remix-ide` directory.
```bash ```bash
npm run serve:production npm run serve:production
``` ```
production build will be served by default to `http://localhost:8080/` or `http://127.0.0.1:8080/` Production build will be served by default to `http://localhost:8080/` or `http://127.0.0.1:8080/`
## Docker: ## Docker:
@ -99,9 +108,9 @@ To fetch docker-compose file without cloning this repo run:
curl https://raw.githubusercontent.com/ethereum/remix-project/master/docker-compose.yaml > docker-compose.yaml curl https://raw.githubusercontent.com/ethereum/remix-project/master/docker-compose.yaml > docker-compose.yaml
``` ```
### Troubleshooting building ### Troubleshooting
If you have trouble building the package, Make sure that you have the correct version of `node`, `npm` and `nvm`. Also ensure you have [Nx CLI](https://nx.dev/react/cli/overview) installed globally. If you have trouble building the project, make sure that you have the correct version of `node`, `npm` and `nvm`. Also ensure [Nx CLI](https://nx.dev/react/cli/overview) is installed globally.
Run: Run:
@ -115,22 +124,23 @@ In Debian based OS such as Ubuntu 14.04LTS you may need to run `apt-get install
## Unit Testing ## Unit Testing
Run the unit tests via: `nx test <project-name>` Run the unit tests using library name like: `nx test <project-name>`
```bash
nx test remix-analyzer
```
Running unit tests via `nx test` requires at least node v10.0.0 For example, to run unit tests of `remix-analyzer`, use `nx test remix-analyzer`
## Browser Testing ## Browser Testing
To run the Selenium tests via Nightwatch: To run the Selenium tests via Nightwatch:
- Build Remix IDE and serve it: `nx build remix-ide --with-deps && nx serve` # starts web server at localhost:8080 - Install Selenium for first time: `npm run selenium-install`
- Make sure Selenium is installed `npm run selenium-install` # don't need to repeat - Run a selenium server: `npm run selenium`
- Run a selenium server `npm run selenium` - Build & Serve Remix: `nx serve`
- Run all the tests `npm run nightwatch_local_firefox` or `npm run nightwatch_local_chrome` - Run all the end-to-end tests:
- Or run a specific test case:
for Firefox: `npm run nightwatch_local_firefox`, or
for Google Chrome: `npm run nightwatch_local_chrome`
- Run a specific test case instead, use one of following commands:
- npm run nightwatch_local_ballot - npm run nightwatch_local_ballot
@ -185,16 +195,14 @@ To run the Selenium tests via Nightwatch:
**NOTE:** **NOTE:**
- **the `ballot` tests suite** requires to run `ganache-cli` locally. - **The `ballot` tests suite** requires to run `ganache-cli` locally.
- **the `remixd` tests suite** requires to run `remixd` locally. - **The `remixd` tests suite** requires to run `remixd` locally.
- **the `gist` tests suite** requires specifying a github access token in **.env file**. - **The `gist` tests suite** requires specifying a github access token in **.env file**.
``` ```
gist_token = <token> gist_token = <token> // token should have permission to create a gist
``` ```
**note that this token should have permission to create a gist.**
## Important Links ## Important Links

@ -4,6 +4,9 @@ import { CompilationOutput, Sources } from '@remix-ui/debugger-ui'
import type { CompilationResult } from '@remix-project/remix-solidity-ts' import type { CompilationResult } from '@remix-project/remix-solidity-ts'
export const DebuggerApiMixin = (Base) => class extends Base { export const DebuggerApiMixin = (Base) => class extends Base {
initialWeb3
initDebuggerApi () { initDebuggerApi () {
this.debugHash = null this.debugHash = null
@ -16,6 +19,8 @@ export const DebuggerApiMixin = (Base) => class extends Base {
} }
} }
this._web3 = new Web3(this.web3Provider) this._web3 = new Web3(this.web3Provider)
// this._web3 can be overwritten and reset to initial value in 'debug' method
this.initialWeb3 = this._web3
remixDebug.init.extendWeb3(this._web3) remixDebug.init.extendWeb3(this._web3)
this.offsetToLineColumnConverter = { this.offsetToLineColumnConverter = {
@ -39,7 +44,7 @@ export const DebuggerApiMixin = (Base) => class extends Base {
} }
async highlight (lineColumnPos, path) { async highlight (lineColumnPos, path) {
await this.call('editor', 'highlight', lineColumnPos, path) await this.call('editor', 'highlight', lineColumnPos, path, '', { focus: true })
} }
async getFile (path) { async getFile (path) {
@ -123,7 +128,9 @@ export const DebuggerApiMixin = (Base) => class extends Base {
debug (hash, web3?) { debug (hash, web3?) {
this.debugHash = hash this.debugHash = hash
if (web3) remixDebug.init.extendWeb3(web3) if (web3) this._web3 = web3
else this._web3 = this.initialWeb3
remixDebug.init.extendWeb3(this._web3)
if (this.onDebugRequestedListener) this.onDebugRequestedListener(hash, web3) if (this.onDebugRequestedListener) this.onDebugRequestedListener(hash, web3)
} }

@ -0,0 +1,30 @@
import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'
class AcceptAndRemember extends EventEmitter {
command (this: NightwatchBrowser, remember:boolean, accept: boolean): NightwatchBrowser {
this.api.perform((done) => {
acceptAndRemember(this.api, remember, accept, () => {
done()
this.emit('complete')
})
})
return this
}
}
function acceptAndRemember (browser: NightwatchBrowser, remember: boolean, accept: boolean, callback: VoidFunction) {
browser.useXpath().waitForElementVisible('//*[@data-id="modalDialogModalBody"]')
if (remember) {
browser.click('//*[@id="remember"]', () => {
if (accept) {
browser.click('//*[@id="modal-footer-ok"]')
} else {
browser.click('//*[@id="modal-footer-cancel"]')
}
browser.perform(function () { callback() })
})
}
}
module.exports = AcceptAndRemember

@ -17,16 +17,25 @@ function addFile (browser: NightwatchBrowser, name: string, content: NightwatchC
browser.clickLaunchIcon('udapp') browser.clickLaunchIcon('udapp')
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory .click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory
.click('.newFile') .elements('css selector', `li[data-id="treeViewLitreeViewItem${name}"]`, (res) => {
.waitForElementContainsText('*[data-id$="/blank"]', '', 60000) if (res.value && (res.value as any).length > 0) {
.sendKeys('*[data-id$="/blank"] .remixui_items', name) browser.openFile(name)
.sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER) .perform(function () {
.pause(2000) done()
.waitForElementVisible(`li[data-id="treeViewLitreeViewItem${name}"]`, 60000) })
.setEditorValue(content.content) } else {
.pause(1000) browser.click('[data-id="fileExplorerNewFilecreateNewFile"]')
.perform(function () { .waitForElementContainsText('*[data-id$="/blank"]', '', 60000)
done() .sendKeys('*[data-id$="/blank"] .remixui_items', name)
.sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER)
.pause(2000)
.waitForElementVisible(`li[data-id="treeViewLitreeViewItem${name}"]`, 60000)
.setEditorValue(content.content)
.pause(1000)
.perform(function () {
done()
})
}
}) })
} }

@ -0,0 +1,46 @@
import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'
import { ExternalProfile, LocationProfile, Profile } from '@remixproject/plugin-utils'
class AddLocalPlugin extends EventEmitter {
command (this: NightwatchBrowser, profile: Profile & LocationProfile & ExternalProfile): NightwatchBrowser {
this.api.perform((done) => {
addLocalPlugin(this.api, profile, () => {
done()
this.emit('complete')
})
})
return this
}
}
function addLocalPlugin (browser: NightwatchBrowser, profile: Profile & LocationProfile & ExternalProfile, callback: VoidFunction) {
browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]')
.pause(3000).element('css selector', '*[data-id="pluginManagerComponentPluginManager"]', function (result) {
if (result.status === 0) {
browser.click('*[plugin="pluginManager"]')
}
})
browser.waitForElementVisible('*[data-id="pluginManagerComponentPluginManager"]')
.execute(function () {
window.testmode = true
})
.click('*[data-id="pluginManagerComponentPluginSearchButton"]')
.waitForElementVisible('*[data-id="pluginManagerLocalPluginModalDialogModalDialogContainer-react"]')
.click('*[data-id="pluginManagerLocalPluginModalDialogModalDialogModalBody-react"]')
.waitForElementVisible('*[data-id="localPluginName"]')
.clearValue('*[data-id="localPluginName"]').setValue('*[data-id="localPluginName"]', profile.name)
.clearValue('*[data-id="localPluginDisplayName"]').setValue('*[data-id="localPluginDisplayName"]', profile.displayName)
.clearValue('*[data-id="localPluginUrl"]').setValue('*[data-id="localPluginUrl"]', profile.url)
.clearValue('*[data-id="localPluginCanActivate"]').setValue('*[data-id="localPluginCanActivate"]', profile.canActivate ? profile.canActivate.join(',') : '')
.click('*[data-id="localPluginRadioButtoniframe"]')
.click(profile.location === 'sidePanel' ? '*[data-id="localPluginRadioButtonsidePanel"]' : '*[data-id="localPluginRadioButtonmainPanel"]')
.click('*[data-id="pluginManagerLocalPluginModalDialogModalDialogModalFooter-react"]')
.click('*[data-id="pluginManagerLocalPluginModalDialog-modal-footer-ok-react')
.waitForElementVisible('[data-id="verticalIconsKindlocalPlugin"]')
.click('[data-id="verticalIconsKindlocalPlugin"]')
.perform(function () { callback() })
}
module.exports = AddLocalPlugin

@ -3,7 +3,7 @@ import { NightwatchBrowser } from 'nightwatch'
class checkAnnotations extends EventEmitter { class checkAnnotations extends EventEmitter {
command (this: NightwatchBrowser, type: string, line: number): NightwatchBrowser { command (this: NightwatchBrowser, type: string, line: number): NightwatchBrowser {
this.api.assert.containsText(`.ace_${type}`, line.toString()).perform(() => this.emit('complete')) this.api.assert.containsText(`.margin-view-overlays .${type} + div`, line.toString()).perform(() => this.emit('complete'))
return this return this
} }
} }

@ -3,7 +3,7 @@ import { NightwatchBrowser } from 'nightwatch'
class checkAnnotationsNotPresent extends EventEmitter { class checkAnnotationsNotPresent extends EventEmitter {
command (this: NightwatchBrowser, type: string): NightwatchBrowser { command (this: NightwatchBrowser, type: string): NightwatchBrowser {
this.api.waitForElementNotPresent(`.ace_${type}`).perform(() => this.emit('complete')) this.api.waitForElementNotPresent(`.margin-view-overlays .${type}`).perform(() => this.emit('complete'))
return this return this
} }
} }

@ -1,22 +0,0 @@
import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'
// fix for editor scroll
class ScrollEditor extends EventEmitter {
command (this: NightwatchBrowser, direction: 'up' | 'down', numberOfTimes: number): NightwatchBrowser {
const browser = this.api
browser.waitForElementPresent('.ace_text-input')
for (let i = 0; i < numberOfTimes; i++) {
if (direction.toLowerCase() === 'up') browser.sendKeys('.ace_text-input', browser.Keys.ARROW_UP)
if (direction.toLowerCase() === 'down') browser.sendKeys('.ace_text-input', browser.Keys.ARROW_DOWN)
}
browser.perform((done) => {
done()
this.emit('complete')
})
return this
}
}
module.exports = ScrollEditor

@ -6,6 +6,7 @@ class ExecuteScript extends EventEmitter {
this.api this.api
.clearEditableContent('*[data-id="terminalCliInput"]') .clearEditableContent('*[data-id="terminalCliInput"]')
.click('*[data-id="terminalCli"]') .click('*[data-id="terminalCli"]')
.setValue('*[data-id="terminalCliInput"]', [this.api.Keys.CONTROL, 'a', this.api.Keys.DELETE])
.sendKeys('*[data-id="terminalCliInput"]', script) .sendKeys('*[data-id="terminalCliInput"]', script)
.sendKeys('*[data-id="terminalCliInput"]', this.api.Keys.ENTER) .sendKeys('*[data-id="terminalCliInput"]', this.api.Keys.ENTER)
.sendKeys('*[data-id="terminalCliInput"]', this.api.Keys.ENTER) .sendKeys('*[data-id="terminalCliInput"]', this.api.Keys.ENTER)

@ -5,9 +5,9 @@ class GetEditorValue extends EventEmitter {
command (this: NightwatchBrowser, callback: (content: string) => void): NightwatchBrowser { command (this: NightwatchBrowser, callback: (content: string) => void): NightwatchBrowser {
this.api.perform((client, done) => { this.api.perform((client, done) => {
this.api.execute(function () { this.api.execute(function () {
const elem: any = document.getElementById('input') const elem: any = document.getElementById('editorView')
return elem.editor.getValue() return elem.currentContent()
}, [], (result) => { }, [], (result) => {
done() done()
const value = typeof result.value === 'string' ? result.value : null const value = typeof result.value === 'string' ? result.value : null

@ -7,11 +7,12 @@ import EventEmitter from 'events'
class JournalLastChildIncludes extends EventEmitter { class JournalLastChildIncludes extends EventEmitter {
command (this: NightwatchBrowser, val: string): NightwatchBrowser { command (this: NightwatchBrowser, val: string): NightwatchBrowser {
this.api this.api
.waitForElementVisible('*[data-id="terminalJournal"] > div:last-child', 10000) .waitForElementVisible('*[data-id="terminalJournal"]', 10000)
.getText('*[data-id="terminalJournal"] > div:last-child', (result) => { .pause(1000)
.getText('*[data-id="terminalJournal"]', (result) => {
console.log('JournalLastChildIncludes', result.value) console.log('JournalLastChildIncludes', result.value)
if (typeof result.value === 'string' && result.value.indexOf(val) === -1) return this.api.assert.fail(`wait for ${val} in ${result.value}`) if (typeof result.value === 'string' && result.value.indexOf(val) === -1) return this.api.assert.fail(`wait for ${val} in ${result.value}`)
else this.api.assert.ok(true, `<*[data-id="terminalJournal"] > div:last-child> contains ${val}.`) else this.api.assert.ok(true, `<*[data-id="terminalJournal"]> contains ${val}.`)
this.emit('complete') this.emit('complete')
}) })
return this return this

@ -39,8 +39,8 @@ function removeFile (browser: NightwatchBrowser, path: string, workspace: string
.pause(2000) .pause(2000)
.perform(() => { .perform(() => {
console.log(path, 'to remove') console.log(path, 'to remove')
browser.waitForElementVisible('*[data-id="' + workspace + 'ModalDialogContainer-react"] .modal-ok') browser.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.click('*[data-id="' + workspace + 'ModalDialogContainer-react"] .modal-ok') .click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.waitForElementNotPresent('[data-path="' + path + '"]') .waitForElementNotPresent('[data-path="' + path + '"]')
done() done()
}) })

@ -0,0 +1,17 @@
import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'
class ScrollToLine extends EventEmitter {
command (this: NightwatchBrowser, line: number): NightwatchBrowser {
this.api.execute(function (line) {
const elem: any = document.getElementById('editorView')
elem.gotoLine(line)
}, [line], () => {
this.emit('complete')
})
return this
}
}
module.exports = ScrollToLine

@ -5,9 +5,9 @@ class SetEditorValue extends EventEmitter {
command (this: NightwatchBrowser, value: string, callback?: VoidFunction): NightwatchBrowser { command (this: NightwatchBrowser, value: string, callback?: VoidFunction): NightwatchBrowser {
this.api.perform((client, done) => { this.api.perform((client, done) => {
this.api.execute(function (value) { this.api.execute(function (value) {
const elem: any = document.getElementById('input') const elem: any = document.getElementById('editorView')
elem.editor.session.setValue(value) elem.setCurrentContent(value)
}, [value], () => { }, [value], () => {
done() done()
if (callback) { if (callback) {

@ -24,8 +24,8 @@ class TestFunction extends EventEmitter {
.perform((done) => { .perform((done) => {
browser.waitForElementVisible(`[data-id="block_tx${txHash}"]`, 60000) browser.waitForElementVisible(`[data-id="block_tx${txHash}"]`, 60000)
.click(`[data-id="block_tx${txHash}"]`) .click(`[data-id="block_tx${txHash}"]`)
.pause(3000)
.waitForElementVisible(`*[data-id="txLoggerTable${txHash}"]`, 60000) .waitForElementVisible(`*[data-id="txLoggerTable${txHash}"]`, 60000)
.pause(10000)
// fetch and format transaction logs as key => pair object // fetch and format transaction logs as key => pair object
.elements('css selector', `*[data-shared="key_${txHash}"]`, (res) => { .elements('css selector', `*[data-shared="key_${txHash}"]`, (res) => {
Array.isArray(res.value) && res.value.forEach(function (jsonWebElement) { Array.isArray(res.value) && res.value.forEach(function (jsonWebElement) {
@ -58,7 +58,6 @@ class TestFunction extends EventEmitter {
.perform(() => { .perform(() => {
Object.keys(expectedValue).forEach(key => { Object.keys(expectedValue).forEach(key => {
let equal = false let equal = false
try { try {
const receivedValue = JSON.parse(logs[key]) const receivedValue = JSON.parse(logs[key])

@ -17,6 +17,7 @@ function verifyContracts (browser: NightwatchBrowser, compiledContractNames: str
browser browser
.clickLaunchIcon('solidity') .clickLaunchIcon('solidity')
.pause(opts.wait) .pause(opts.wait)
.pause(5000)
.waitForElementPresent('*[data-id="compiledContracts"] option', 60000) .waitForElementPresent('*[data-id="compiledContracts"] option', 60000)
.perform((done) => { .perform((done) => {
if (opts.version) { if (opts.version) {

@ -0,0 +1,13 @@
import { PluginClient } from '@remixproject/plugin'
import { createClient } from '@remixproject/plugin-webview'
export class RemixPlugin extends PluginClient {
constructor () {
super()
this.methods = ['testCommand']
createClient(this)
}
async testCommand (data: any) {
}
}

@ -1,128 +1,3 @@
.app { .jumbotron {
font-family: sans-serif; max-height: 25vh;
min-width: 300px;
max-width: 600px;
margin: 50px auto;
}
.app .gutter-left {
margin-left: 9px;
}
.app .col-span-2 {
grid-column: span 2;
}
.app .flex {
display: flex;
align-items: center;
justify-content: center;
}
.app header {
background-color: #143055;
color: white;
padding: 5px;
border-radius: 3px;
}
.app main {
padding: 0 36px;
}
.app p {
text-align: center;
}
.app h1 {
text-align: center;
margin-left: 18px;
font-size: 24px;
}
.app h2 {
text-align: center;
font-size: 20px;
margin: 40px 0 10px 0;
}
.app .resources {
text-align: center;
list-style: none;
padding: 0;
display: grid;
grid-gap: 9px;
grid-template-columns: 1fr 1fr;
}
.app .resource {
color: #0094ba;
height: 36px;
background-color: rgba(0, 0, 0, 0);
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 4px;
padding: 3px 9px;
text-decoration: none;
}
.app .resource:hover {
background-color: rgba(68, 138, 255, 0.04);
}
.app pre {
padding: 9px;
border-radius: 4px;
background-color: black;
color: #eee;
}
.app details {
border-radius: 4px;
color: #333;
background-color: rgba(0, 0, 0, 0);
border: 1px solid rgba(0, 0, 0, 0.12);
padding: 3px 9px;
margin-bottom: 9px;
}
.app summary {
outline: none;
height: 36px;
line-height: 36px;
}
.app .github-star-container {
margin-top: 12px;
line-height: 20px;
}
.app .github-star-container a {
display: flex;
align-items: center;
text-decoration: none;
color: #333;
}
.app .github-star-badge {
color: #24292e;
display: flex;
align-items: center;
font-size: 12px;
padding: 3px 10px;
border: 1px solid rgba(27, 31, 35, 0.2);
border-radius: 3px;
background-image: linear-gradient(-180deg, #fafbfc, #eff3f6 90%);
margin-left: 4px;
font-weight: 600;
}
.app .github-star-badge:hover {
background-image: linear-gradient(-180deg, #f0f3f6, #e6ebf1 90%);
border-color: rgba(27, 31, 35, 0.35);
background-position: -0.5em;
}
.app .github-star-badge .material-icons {
height: 16px;
width: 16px;
margin-right: 4px;
} }

@ -1,37 +1,126 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { PluginClient } from '@remixproject/plugin' import { RemixPlugin } from './Client'
import { createClient } from '@remixproject/plugin-webview' import { Logger } from './logger'
import { filePanelProfile } from '@remixproject/plugin-api/lib/file-system/file-panel/profile'
import { filSystemProfile } from '@remixproject/plugin-api/lib/file-system/file-manager/profile'
import { dGitProfile } from '@remixproject/plugin-api/lib/dgit/profile'
import { editorProfile } from '@remixproject/plugin-api/lib/editor/profile'
import { settingsProfile } from '@remixproject/plugin-api/lib/settings/profile'
import { networkProfile } from '@remixproject/plugin-api/lib/network/profile'
import { terminalProfile } from '@remixproject/plugin-api/lib/terminal/profile'
import { udappProfile } from '@remixproject/plugin-api/lib/udapp'
import { compilerProfile } from '@remixproject/plugin-api/lib/compiler'
import { contentImportProfile } from '@remixproject/plugin-api/lib/content-import'
import { windowProfile } from '@remixproject/plugin-api/lib/window'
import { pluginManagerProfile } from '@remixproject/plugin-api/lib/plugin-manager'
import { Profile } from '@remixproject/plugin-utils'
import './app.css' import './app.css'
import { ReactComponent as Logo } from './logo.svg' const client = new RemixPlugin()
function App () {
const [payload, setPayload] = useState<string>('')
const [log, setLog] = useState<any>()
const [started, setStarted] = useState<boolean>(false)
const [events, setEvents] = useState<any>()
const [profiles, setProfiles] = useState<Profile[]>([pluginManagerProfile, filePanelProfile, filSystemProfile, dGitProfile, networkProfile, settingsProfile, editorProfile, terminalProfile, compilerProfile, udappProfile, contentImportProfile, windowProfile])
export const App = () => { const handleChange = ({ target }: any) => {
const [remixClient, setRemixClient] = useState(null) setPayload(target.value)
}
useEffect(() => { useEffect(() => {
(async () => { client.onload(async () => {
const client = createClient(new PluginClient()) const customProfiles = ['menuicons', 'tabs', 'solidityUnitTesting']
client.testCommand = async (data: any) => {
console.log(data)
methodLog(data)
}
let addProfiles = []
for (const name of customProfiles) {
const p = await client.call('manager', 'getProfile', name)
addProfiles = [...addProfiles, p]
}
setProfiles(profiles => [...profiles, ...addProfiles])
await client.onload() profiles.map((profile: Profile) => {
console.log('Local plugin loaded') if (profile.events) {
setRemixClient(client) profile.events.map((event: string) => {
})() client.on(profile.name as any, event, (...args: any) => {
console.log('event :', event, args)
eventLog({
event: event,
args: args
})
})
})
}
})
})
}, []) }, [])
const handleClick = () => { const methodLog = (log: any) => {
remixClient.call('manager', 'activatePlugin', 'LearnEth') const addValue = typeof log === 'string' ? log : JSON.stringify(log)
setLog((value) => `${value} ${addValue}`)
}
const eventLog = (log: any) => {
const addValue = typeof log === 'string' ? log : JSON.stringify(log)
setEvents((value) => `${value} ${addValue}`)
}
const clientMethod = async (profile: Profile, method: string) => {
try {
let ob: any = null
try {
ob = JSON.parse(payload)
if (ob && !Array.isArray(ob)) { ob = [ob] }
} catch (e) { }
const args = ob || [payload]
setStarted(true)
setLog('')
setEvents('')
console.log('calling :', profile.name, method, ...args)
await client.call('manager', 'activatePlugin', profile.name)
const result = await client.call(profile.name as any, method, ...args)
console.log('result :', result)
methodLog(result)
} catch (e) {
methodLog(e.message)
}
setStarted(false)
} }
return ( return (
<div className="app"> <div className="App container-fluid">
<header className="flex"> <h5>PLUGIN API TESTER</h5>
<Logo width="75" height="75" /> <label id='callStatus'>{started ? <>start</> : <>stop</> }</label><br></br>
<h1>Welcome to local-plugin!</h1> <label>method results</label>
</header> <Logger id='methods' log={log}></Logger>
<main> <label>events</label>
<button data-id="btnActivateRemixd" onClick={handleClick}>Activate Learneth</button> <Logger id='events' log={events}></Logger>
</main> <input
className='form-control w-100'
type="text"
id="payload"
placeholder="Enter payload here..."
value={payload}
onChange={handleChange}
/>
{profiles.map((profile: Profile) => {
const methods = profile.methods.map((method: string) => {
return <button data-id={`${profile.name}:${method}`} key={method} className='btn btn-primary btn-sm ml-1 mb-1' onClick={async () => await clientMethod(profile, method)}>{method}</button>
})
const events = profile.events ? profile.events.map((event: string) => {
return <label key={event} className='m-1'>{event}</label>
}) : null
return <div key={profile.name} className='small border-bottom'><label className='text-uppercase'>{profile.name}</label><br></br>{methods}<br></br>{events ? <label>EVENTS:</label> : null}{events}</div>
})}
</div> </div>
) )
} }

@ -0,0 +1,9 @@
import React from 'react'
interface loggerProps {
log: any,
id: string
}
export const Logger: React.FC<loggerProps> = (props) => {
return (<div id={props.id} className="jumbotron overflow-auto text-break mb-1 p-2">{props.log}</div>)
}

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="262px" height="163px" viewBox="0 0 262 163" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Styles-&amp;-Quick-Wins" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Nx---Quick-Wins" transform="translate(-476.000000, -1284.000000)" fill-rule="nonzero">
<g id="Logos" transform="translate(-11.000000, 782.000000)">
<g id="Nx_Flat_White" transform="translate(487.000000, 502.000000)">
<polygon id="Path" fill="#FFFFFF" points="130.68 104.59 97.49 52.71 97.44 96.3 40.24 0 0 0 0 162.57 39.79 162.57 39.92 66.39 96.53 158.26"></polygon>
<polygon id="Path" fill="#FFFFFF" points="97.5 41.79 137.24 41.79 137.33 41.33 137.33 0 97.54 0 97.49 41.33"></polygon>
<path d="M198.66,86.86 C189.139872,86.6795216 180.538723,92.516445 177.19,101.43 C182.764789,93.0931021 193.379673,89.7432211 202.73,93.37 C207.05,95.13 212.73,97.97 217.23,96.45 C212.950306,90.4438814 206.034895,86.8725952 198.66,86.86 L198.66,86.86 Z" id="Path" fill="#96D8E9"></path>
<path d="M243.75,106.42 C243.75,101.55 241.1,100.42 235.6,98.42 C231.52,97 226.89,95.4 223.52,91 C222.86,90.13 222.25,89.15 221.6,88.11 C220.14382,85.4164099 218.169266,83.037429 215.79,81.11 C212.58,78.75 208.37,77.6 202.91,77.6 C191.954261,77.6076705 182.084192,84.2206169 177.91,94.35 C183.186964,87.0278244 191.956716,83.0605026 200.940147,83.9314609 C209.923578,84.8024193 217.767888,90.3805017 221.54,98.58 C223.424615,101.689762 227.141337,103.174819 230.65,102.22 C236.02,101.07 235.65,106.15 243.76,107.87 L243.75,106.42 Z" id="Path" fill="#48C4E5"></path>
<path d="M261.46,105.38 L261.46,105.27 C261.34,73.03 235.17,45.45 202.91,45.45 C183.207085,45.4363165 164.821777,55.3450614 154,71.81 L153.79,71.45 L137.23,45.45 L97.5,45.4499858 L135.25,104.57 L98.41,162.57 L137,162.57 L153.79,136.78 L170.88,162.57 L209.48,162.57 L174.48,107.49 C173.899005,106.416838 173.583536,105.220114 173.56,104 C173.557346,96.2203871 176.64661,88.7586448 182.147627,83.2576275 C187.648645,77.7566101 195.110387,74.6673462 202.89,74.67 C219.11,74.67 221.82,84.37 225.32,88.93 C232.23,97.93 246.03,93.99 246.03,105.73 L246.03,105.73 C246.071086,108.480945 247.576662,111.001004 249.979593,112.340896 C252.382524,113.680787 255.317747,113.636949 257.679593,112.225896 C260.041438,110.814842 261.471086,108.250945 261.43,105.5 L261.43,105.5 L261.43,105.38 L261.46,105.38 Z" id="Path" fill="#FFFFFF"></path>
<path d="M261.5,113.68 C261.892278,116.421801 261.504116,119.218653 260.38,121.75 C258.18,126.84 254.51,125.14 254.51,125.14 C254.51,125.14 251.35,123.6 253.27,120.65 C255.4,117.36 259.61,117.74 261.5,113.68 Z" id="Path" fill="#FFFFFF"></path>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg
className="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
</svg>

Before

Width:  |  Height:  |  Size: 347 B

@ -1,10 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" />
<title>LocalPlugin</title>
<base href="/" /> <base href="/" />
<meta charset="utf-8" />
<title>Remix Plugin API Tester</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="favicon.ico" />
</head> </head>

@ -56,8 +56,7 @@ module.exports = {
.waitForElementVisible('#stepdetail') .waitForElementVisible('#stepdetail')
.goToVMTraceStep(144) .goToVMTraceStep(144)
.pause(2000) .pause(2000)
// Should be uncommented while fixing https://github.com/ethereum/remix-project/issues/1644 .checkVariableDebug('soliditystate', stateCheck)
// .checkVariableDebug('soliditystate', stateCheck)
.checkVariableDebug('soliditylocals', localsCheck) .checkVariableDebug('soliditylocals', localsCheck)
}, },
@ -82,11 +81,20 @@ module.exports = {
'Deploy and use Ballot using external web3': function (browser: NightwatchBrowser) { 'Deploy and use Ballot using external web3': function (browser: NightwatchBrowser) {
browser browser
.openFile('Untitled.sol')
.clickLaunchIcon('udapp')
.click('*[data-id="settingsWeb3Mode"]') .click('*[data-id="settingsWeb3Mode"]')
.modalFooterOKClick() .modalFooterOKClick()
.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('solidity')
.testContracts('Untitled.sol', sources[0]['Untitled.sol'], ['Ballot'])
.clickLaunchIcon('udapp') .clickLaunchIcon('udapp')
.pause(2000)
.setValue('input[placeholder="bytes32[] proposalNames"]', '["0x48656c6c6f20576f726c64210000000000000000000000000000000000000000"]') .setValue('input[placeholder="bytes32[] proposalNames"]', '["0x48656c6c6f20576f726c64210000000000000000000000000000000000000000"]')
.click('*[data-id="Deploy - transact (not payable)"]') .click('*[data-id="Deploy - transact (not payable)"]')
.clickInstance(0) .clickInstance(0)
@ -117,7 +125,7 @@ const localsCheck = {
type: 'address' type: 'address'
} }
} }
/*
const stateCheck = { const stateCheck = {
chairperson: { chairperson: {
value: '0xCA35B7D915458EF540ADE6068DFE2F44E8FA733C', value: '0xCA35B7D915458EF540ADE6068DFE2F44E8FA733C',
@ -175,8 +183,6 @@ const stateCheck = {
immutable: false immutable: false
} }
} }
*/
const ballotABI = `[ const ballotABI = `[
{ {
"inputs": [ "inputs": [

@ -28,7 +28,6 @@ module.exports = {
'Deploy Ballot': function (browser: NightwatchBrowser) { 'Deploy Ballot': function (browser: NightwatchBrowser) {
browser.pause(500) browser.pause(500)
.testContracts('Untitled.sol', sources[0]['Untitled.sol'], ['Ballot'])
.clickLaunchIcon('udapp') .clickLaunchIcon('udapp')
.selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') .selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c')
.setValue('input[placeholder="uint8 _numProposals"]', '2') .setValue('input[placeholder="uint8 _numProposals"]', '2')
@ -77,11 +76,13 @@ module.exports = {
'Deploy and use Ballot using external web3': function (browser: NightwatchBrowser) { 'Deploy and use Ballot using external web3': function (browser: NightwatchBrowser) {
browser browser
.openFile('Untitled.sol')
.clickLaunchIcon('udapp')
.click('*[data-id="settingsWeb3Mode"]') .click('*[data-id="settingsWeb3Mode"]')
.modalFooterOKClick() .modalFooterOKClick()
.clickLaunchIcon('solidity') .clickLaunchIcon('solidity')
.testContracts('Untitled.sol', sources[0]['Untitled.sol'], ['Ballot'])
.clickLaunchIcon('udapp') .clickLaunchIcon('udapp')
.pause(2000)
.setValue('input[placeholder="uint8 _numProposals"]', '2') .setValue('input[placeholder="uint8 _numProposals"]', '2')
.click('*[data-id="Deploy - transact (not payable)"]') .click('*[data-id="Deploy - transact (not payable)"]')
.clickInstance(0) .clickInstance(0)

@ -21,7 +21,7 @@ module.exports = {
browser browser
.addFile('test_jsCompile.js', { content: jsCompile }) .addFile('test_jsCompile.js', { content: jsCompile })
.executeScript('remix.exeCurrent()') .executeScript('remix.exeCurrent()')
.waitForElementContainsText('*[data-id="terminalJournal"]', '"languageversion": "0.6.8+commit.0bbfe453"', 60000) .waitForElementContainsText('*[data-id="terminalJournal"]', '"languageversion":"0.6.8+commit.0bbfe453"', 60000)
.click('*[data-id="terminalClearConsole"]') .click('*[data-id="terminalClearConsole"]')
}, },

@ -62,9 +62,13 @@ module.exports = {
}, },
'Should jump through breakpoints': function (browser: NightwatchBrowser) { 'Should jump through breakpoints': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]') browser.waitForElementVisible('#editorView')
.click('.ace_gutter-cell:nth-of-type(10)') .execute(() => {
.click('.ace_gutter-cell:nth-of-type(20)') (window as any).addRemixBreakpoint(11)
}, [], () => {})
.execute(() => {
(window as any).addRemixBreakpoint(21)
}, [], () => {})
.waitForElementVisible('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]') .waitForElementVisible('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]')
.click('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]') .click('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]')
.pause(2000) .pause(2000)
@ -127,7 +131,7 @@ module.exports = {
But the debugger uses now validSourcelocation, which means file is not -1. But the debugger uses now validSourcelocation, which means file is not -1.
In that case the source highlight at 261 should be the same as for step 262 In that case the source highlight at 261 should be the same as for step 262
*/ */
.waitForElementPresent('.highlightLine7') .waitForElementPresent('.highlightLine8')
.goToVMTraceStep(266) .goToVMTraceStep(266)
.pause(1000) .pause(1000)
.checkVariableDebug('soliditylocals', localVariable_step266_ABIEncoder) // locals should not be initiated at this point, only idAsk should .checkVariableDebug('soliditylocals', localVariable_step266_ABIEncoder) // locals should not be initiated at this point, only idAsk should
@ -187,7 +191,8 @@ module.exports = {
browser browser
.addFile('test_jsGetTrace.js', { content: jsGetTrace }) .addFile('test_jsGetTrace.js', { content: jsGetTrace })
.executeScript('remix.exeCurrent()') .executeScript('remix.exeCurrent()')
.waitForElementContainsText('*[data-id="terminalJournal"]', 'result { "gas": "0x575f", "return": "0x0000000000000000000000000000000000000000000000000000000000000000", "structLogs":', 60000) .pause(1000)
.waitForElementContainsText('*[data-id="terminalJournal"]', '{"gas":"0x575f","return":"0x0000000000000000000000000000000000000000000000000000000000000000","structLogs":', 60000)
}, },
'Should call the debugger api: debug': function (browser: NightwatchBrowser) { 'Should call the debugger api: debug': function (browser: NightwatchBrowser) {

@ -50,11 +50,11 @@ module.exports = {
'Toggles Terminal': function (browser: NightwatchBrowser) { 'Toggles Terminal': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('div[data-id="terminalContainer"]') browser.waitForElementVisible('div[data-id="terminalContainer"]')
.assert.visible('div[data-id="terminalContainerDisplay"]') .assert.elementPresent('div[data-id="terminalContainerDisplay"]')
.click('i[data-id="terminalToggleIcon"]') .click('i[data-id="terminalToggleIcon"]')
.checkElementStyle('div[data-id="terminalToggleMenu"]', 'height', '35px') .checkElementStyle('div[data-id="terminalToggleMenu"]', 'height', '35px')
.click('i[data-id="terminalToggleIcon"]') .click('i[data-id="terminalToggleIcon"]')
.assert.visible('div[data-id="terminalContainerDisplay"]') .assert.elementPresent('div[data-id="terminalContainerDisplay"]')
}, },
'Switch Tabs using tabs icon': function (browser: NightwatchBrowser) { 'Switch Tabs using tabs icon': function (browser: NightwatchBrowser) {
@ -63,9 +63,6 @@ module.exports = {
.click('[data-id="treeViewLitreeViewItemcontracts"]') .click('[data-id="treeViewLitreeViewItemcontracts"]')
.openFile('contracts/3_Ballot.sol') .openFile('contracts/3_Ballot.sol')
.assert.containsText('div[title="default_workspace/contracts/3_Ballot.sol"]', '3_Ballot.sol') .assert.containsText('div[title="default_workspace/contracts/3_Ballot.sol"]', '3_Ballot.sol')
.click('span[class^=dropdownCaret]')
.click('#homeItem')
.assert.containsText('div[title="home"]', 'Home')
.end() .end()
} }
} }

@ -15,37 +15,35 @@ module.exports = {
.waitForElementVisible('div[data-id="filePanelFileExplorerTree"]') .waitForElementVisible('div[data-id="filePanelFileExplorerTree"]')
.openFile('contracts') .openFile('contracts')
.openFile('contracts/1_Storage.sol') .openFile('contracts/1_Storage.sol')
.waitForElementVisible('*[data-id="editorInput"]') .waitForElementVisible('#editorView')
.checkElementStyle('*[data-id="editorInput"]', 'font-size', '12px') .checkElementStyle('.view-lines', 'font-size', '14px')
.click('*[data-id="tabProxyZoomIn"]') .click('*[data-id="tabProxyZoomIn"]')
.click('*[data-id="tabProxyZoomIn"]') .click('*[data-id="tabProxyZoomIn"]')
.checkElementStyle('*[data-id="editorInput"]', 'font-size', '14px') .checkElementStyle('.view-lines', 'font-size', '16px')
}, },
'Should zoom out editor': function (browser: NightwatchBrowser) { 'Should zoom out editor': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]') browser.waitForElementVisible('#editorView')
.checkElementStyle('*[data-id="editorInput"]', 'font-size', '14px') .checkElementStyle('.view-lines', 'font-size', '16px')
.click('*[data-id="tabProxyZoomOut"]') .click('*[data-id="tabProxyZoomOut"]')
.click('*[data-id="tabProxyZoomOut"]') .click('*[data-id="tabProxyZoomOut"]')
.checkElementStyle('*[data-id="editorInput"]', 'font-size', '12px') .checkElementStyle('.view-lines', 'font-size', '14px')
}, },
'Should display compile error in editor': function (browser: NightwatchBrowser) { 'Should display compile error in editor': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]') browser.waitForElementVisible('#editorView')
.waitForElementVisible('*[class="ace_content"]') .setEditorValue(storageContractWithError + 'error')
.click('*[class="ace_content"]')
.sendKeys('*[class="ace_text-input"]', 'error')
.pause(2000) .pause(2000)
.waitForElementVisible('.ace_error', 120000) .waitForElementVisible('.margin-view-overlays .fa-exclamation-square', 120000)
.checkAnnotations('error', 28) .checkAnnotations('fa-exclamation-square', 29) // error
.clickLaunchIcon('udapp') .clickLaunchIcon('udapp')
.checkAnnotationsNotPresent('error') .checkAnnotationsNotPresent('fa-exclamation-square') // error
.clickLaunchIcon('solidity') .clickLaunchIcon('solidity')
.checkAnnotations('error', 28) .checkAnnotations('fa-exclamation-square', 29) // error
}, },
'Should minimize and maximize codeblock in editor': function (browser: NightwatchBrowser) { 'Should minimize and maximize codeblock in editor': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]') browser.waitForElementVisible('#editorView')
.waitForElementVisible('.ace_open') .waitForElementVisible('.ace_open')
.click('.ace_start:nth-of-type(1)') .click('.ace_start:nth-of-type(1)')
.waitForElementVisible('.ace_closed') .waitForElementVisible('.ace_closed')
@ -54,27 +52,29 @@ module.exports = {
}, },
'Should add breakpoint to editor': function (browser: NightwatchBrowser) { 'Should add breakpoint to editor': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]') browser.waitForElementVisible('#editorView')
.waitForElementNotPresent('.ace_breakpoint') .waitForElementNotPresent('.margin-view-overlays .fa-circle')
.click('.ace_gutter-cell:nth-of-type(1)') .execute(() => {
.waitForElementVisible('.ace_breakpoint') (window as any).addRemixBreakpoint(1)
}, [], () => {})
.waitForElementVisible('.margin-view-overlays .fa-circle')
}, },
'Should load syntax highlighter for ace light theme': function (browser: NightwatchBrowser) { 'Should load syntax highlighter for ace light theme': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]') browser.waitForElementVisible('#editorView')
.checkElementStyle('.ace_keyword', 'color', aceThemes.light.keyword) .checkElementStyle('.ace_keyword', 'color', aceThemes.light.keyword)
.checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.light.comment) .checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.light.comment)
.checkElementStyle('.ace_function', 'color', aceThemes.light.function) .checkElementStyle('.ace_function', 'color', aceThemes.light.function)
.checkElementStyle('.ace_variable', 'color', aceThemes.light.variable) .checkElementStyle('.ace_variable', 'color', aceThemes.light.variable)
}, },
'Should load syntax highlighter for ace dark theme': function (browser: NightwatchBrowser) { 'Should load syntax highlighter for ace dark theme': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="verticalIconsKindsettings"]') browser.waitForElementVisible('*[data-id="verticalIconsKindsettings"]')
.click('*[data-id="verticalIconsKindsettings"]') .click('*[data-id="verticalIconsKindsettings"]')
.waitForElementVisible('*[data-id="settingsTabThemeLabelDark"]') .waitForElementVisible('*[data-id="settingsTabThemeLabelDark"]')
.click('*[data-id="settingsTabThemeLabelDark"]') .click('*[data-id="settingsTabThemeLabelDark"]')
.pause(2000) .pause(2000)
.waitForElementVisible('*[data-id="editorInput"]') .waitForElementVisible('#editorView')
/* @todo(#2863) ch for class and not colors /* @todo(#2863) ch for class and not colors
.checkElementStyle('.ace_keyword', 'color', aceThemes.dark.keyword) .checkElementStyle('.ace_keyword', 'color', aceThemes.dark.keyword)
.checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.dark.comment) .checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.dark.comment)
@ -87,20 +87,21 @@ module.exports = {
// include all files here because switching between plugins in side-panel removes highlight // include all files here because switching between plugins in side-panel removes highlight
browser browser
.addFile('sourcehighlight.js', sourcehighlightScript) .addFile('sourcehighlight.js', sourcehighlightScript)
.addFile('removeSourcehighlightScript.js', removeSourcehighlightScript)
.addFile('removeAllSourcehighlightScript.js', removeAllSourcehighlightScript) .addFile('removeAllSourcehighlightScript.js', removeAllSourcehighlightScript)
.openFile('sourcehighlight.js') .openFile('sourcehighlight.js')
.executeScript('remix.exeCurrent()') .executeScript('remix.exeCurrent()')
.editorScroll('down', 60) .scrollToLine(32)
.waitForElementPresent('.highlightLine32', 60000) .waitForElementPresent('.highlightLine33', 60000)
.checkElementStyle('.highlightLine32', 'background-color', 'rgb(8, 108, 181)') .checkElementStyle('.highlightLine33', 'background-color', 'rgb(52, 152, 219)')
.waitForElementPresent('.highlightLine40', 60000) .scrollToLine(40)
.checkElementStyle('.highlightLine40', 'background-color', 'rgb(8, 108, 181)') .waitForElementPresent('.highlightLine41', 60000)
.waitForElementPresent('.highlightLine50', 60000) .checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)')
.checkElementStyle('.highlightLine50', 'background-color', 'rgb(8, 108, 181)') .scrollToLine(50)
.waitForElementPresent('.highlightLine51', 60000)
.checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)')
}, },
'Should remove 1 highlight from source code': function (browser: NightwatchBrowser) { 'Should remove 1 highlight from source code': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]') browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]')
.click('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]') .click('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]')
.pause(2000) .pause(2000)
@ -109,9 +110,9 @@ module.exports = {
.click('li[data-id="treeViewLitreeViewItemcontracts"]') .click('li[data-id="treeViewLitreeViewItemcontracts"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') .click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.waitForElementNotPresent('.highlightLine32', 60000) .waitForElementNotPresent('.highlightLine33', 60000)
.checkElementStyle('.highlightLine40', 'background-color', 'rgb(8, 108, 181)') .checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)')
.checkElementStyle('.highlightLine50', 'background-color', 'rgb(8, 108, 181)') .checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)')
}, },
'Should remove all highlights from source code': function (browser: NightwatchBrowser) { 'Should remove all highlights from source code': function (browser: NightwatchBrowser) {
@ -122,9 +123,9 @@ module.exports = {
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') .click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.pause(2000) .pause(2000)
.waitForElementNotPresent('.highlightLine32', 60000) .waitForElementNotPresent('.highlightLine33', 60000)
.waitForElementNotPresent('.highlightLine40', 60000) .waitForElementNotPresent('.highlightLine41', 60000)
.waitForElementNotPresent('.highlightLine50', 60000) .waitForElementNotPresent('.highlightLine51', 60000)
.end() .end()
} }
} }
@ -148,6 +149,7 @@ const sourcehighlightScript = {
content: ` content: `
(async () => { (async () => {
try { try {
await remix.call('fileManager', 'open', 'contracts/3_Ballot.sol')
const pos = { const pos = {
start: { start: {
line: 32, line: 32,
@ -190,18 +192,6 @@ const sourcehighlightScript = {
` `
} }
const removeSourcehighlightScript = {
content: `
(async () => {
try {
await remix.call('editor', 'discardHighlightAt', 32, 'contracts/3_Ballot.sol')
} catch (e) {
console.log(e.message)
}
})()
`
}
const removeAllSourcehighlightScript = { const removeAllSourcehighlightScript = {
content: ` content: `
(async () => { (async () => {
@ -213,3 +203,33 @@ const removeAllSourcehighlightScript = {
})() })()
` `
} }
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;
}
}`

@ -68,9 +68,9 @@ module.exports = {
.waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_E2E_Tests"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_E2E_Tests"]')
.rightClick('[data-path="Browser_E2E_Tests"]') .rightClick('[data-path="Browser_E2E_Tests"]')
.click('*[id="menuitemdelete"]') .click('*[id="menuitemdelete"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]', 60000) .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok', 60000)
.pause(2000) .pause(2000)
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok') .click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemBrowser_E2E_Tests"]') .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemBrowser_E2E_Tests"]')
}, },
@ -81,13 +81,13 @@ module.exports = {
.pause(10000) .pause(10000)
.waitForElementVisible('*[data-id="fileExplorerNewFilepublishToGist"]') .waitForElementVisible('*[data-id="fileExplorerNewFilepublishToGist"]')
.click('*[data-id="fileExplorerNewFilepublishToGist"]') .click('*[data-id="fileExplorerNewFilepublishToGist"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]', 60000) .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok', 60000)
.pause(2000) .pause(2000)
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok') .click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.pause(2000) .pause(2000)
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]', 60000) .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok', 60000)
.pause(2000) .pause(2000)
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok') .click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.pause(2000) .pause(2000)
.perform((done) => { .perform((done) => {
if (runtimeBrowser === 'chrome') { if (runtimeBrowser === 'chrome') {
@ -101,6 +101,7 @@ module.exports = {
'Should open local filesystem explorer': function (browser: NightwatchBrowser) { 'Should open local filesystem explorer': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="filePanelFileExplorerTree"]') browser.waitForElementVisible('*[data-id="filePanelFileExplorerTree"]')
.click('[data-id="remixUIWorkspaceExplorer"]')
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile1) .setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile1)
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile2) .setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile2)
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile3) .setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile3)

@ -11,6 +11,7 @@ module.exports = {
browser browser
.addFile('file.js', { content: executeFile }) .addFile('file.js', { content: executeFile })
.executeScript('remix.exeCurrent()') .executeScript('remix.exeCurrent()')
.pause(1000)
.waitForElementContainsText('*[data-id="terminalJournal"]', 'file.js', 60000) .waitForElementContainsText('*[data-id="terminalJournal"]', 'file.js', 60000)
}, },
@ -35,7 +36,9 @@ module.exports = {
.executeScript('remix.exeCurrent()') .executeScript('remix.exeCurrent()')
.pause(2000) .pause(2000)
.openFile('new_contract.sol') .openFile('new_contract.sol')
.assert.containsText('[data-id="editorInput"]', 'pragma solidity ^0.6.0') .getEditorValue((content) => {
browser.assert.ok(content.indexOf('pragma solidity ^0.6.0') !== -1, 'content does not contain "pragma solidity ^0.6.0"')
})
}, },
'Should execute `readFile` api from file manager external api': function (browser: NightwatchBrowser) { 'Should execute `readFile` api from file manager external api': function (browser: NightwatchBrowser) {
@ -72,7 +75,8 @@ module.exports = {
browser browser
.addFile('readdirFile.js', { content: executeReaddir }) .addFile('readdirFile.js', { content: executeReaddir })
.executeScript('remix.exeCurrent()') .executeScript('remix.exeCurrent()')
.waitForElementContainsText('*[data-id="terminalJournal"]', 'Test_Folder isDirectory true', 60000) .waitForElementContainsText('*[data-id="terminalJournal"]', 'Test_Folder isDirectory', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', 'true', 5000)
}, },
'Should execute `remove` api from file manager external api': function (browser: NightwatchBrowser) { 'Should execute `remove` api from file manager external api': function (browser: NightwatchBrowser) {
@ -175,8 +179,8 @@ const executeMkdir = `
const executeReaddir = ` const executeReaddir = `
const run = async () => { const run = async () => {
const result = await remix.call('fileManager', 'readdir', '/') const result = await remix.call('fileManager', 'readdir', '/')
const output = result["Test_Folder"].isDirectory
console.log('Test_Folder isDirectory ', result["Test_Folder"].isDirectory) console.log('Test_Folder isDirectory ', output)
} }
run() run()

@ -103,14 +103,18 @@ module.exports = {
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="fileExplorerNewFilepublishToGist"]') .waitForElementVisible('*[data-id="fileExplorerNewFilepublishToGist"]')
.click('*[data-id="fileExplorerNewFilepublishToGist"]') .click('*[data-id="fileExplorerNewFilepublishToGist"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]') .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.pause(2000) .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.pause(10000) .pause(10000)
.getText('[data-id="default_workspaceModalDialogModalBody-react"]', (result) => { .perform((done) => {
browser.assert.ok(result.value === 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Assert failed. Gist token error message not displayed.') browser.getText('[data-id="fileSystemModalDialogModalBody-react"]', (result) => {
console.log('result.value: ', result.value)
browser.assert.ok(result.value === 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Assert failed. Gist token error message not displayed.')
done()
})
}) })
.click('[data-id="default_workspace-modal-footer-ok-react"]') .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
}, },
'Import From Gist For Valid Gist ID': function (browser: NightwatchBrowser) { 'Import From Gist For Valid Gist ID': function (browser: NightwatchBrowser) {

@ -77,6 +77,7 @@ function checkDeployShouldFail (browser: NightwatchBrowser, callback: VoidFuncti
.clickLaunchIcon('udapp') .clickLaunchIcon('udapp')
.selectContract('test') // deploy lib .selectContract('test') // deploy lib
.createContract('') .createContract('')
.pause(2000)
.getText('div[class^="terminal"]', (value) => { .getText('div[class^="terminal"]', (value) => {
console.log('value: ', value) console.log('value: ', value)
}) })

@ -12,13 +12,6 @@ const testData = {
pluginUrl: 'https://zokrates.github.io/zokrates-remix-plugin/' pluginUrl: 'https://zokrates.github.io/zokrates-remix-plugin/'
} }
const localPluginData = {
pluginName: 'localPlugin',
pluginDisplayName: 'Local Plugin',
pluginCanActivate: 'LearnEth',
pluginUrl: 'http://localhost:2020'
}
module.exports = { module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) { before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done, 'http://127.0.0.1:8080', false) init(browser, done, 'http://127.0.0.1:8080', false)
@ -76,44 +69,6 @@ module.exports = {
.waitForElementVisible('*[data-id="pluginManagerComponentActivateButtonvyper"]', 60000) .waitForElementVisible('*[data-id="pluginManagerComponentActivateButtonvyper"]', 60000)
}, },
/*
'Should grant plugin permission (ZOKRATES)': function (browser) {
browser.waitForElementVisible('*[data-id="pluginManagerComponentPluginManager"]')
.click('*[data-id="pluginManagerPermissionsButton"]')
.waitForElementVisible('*[data-id="pluginManagerSettingsPermissionForm"]')
.assert.containsText('*[data-id="pluginManagerSettingsPermissionForm"]', 'No Permission requested yet')
.modalFooterOKClick()
.click('*[data-id="verticalIconsFileExplorerIcons"]')
.openFile('3_Ballot.sol')
.click('*[plugin="ZoKrates"]')
.pause(5000)
.frame(0)
.useXpath().click("//span[text()='Compile']")
.pause(2000)
.frameParent()
.useCss().waitForElementVisible('*[data-id="modalDialogContainer"]')
.assert.containsText('*[data-id="permissionHandlerMessage"]', 'ZOKRATES" WOULD LIKE TO ACCESS "FILE MANAGER" :')
.pause(2000)
.click('*[data-id="permissionHandlerRememberChoice"]')
.pause(2000)
.modalFooterOKClick()
},
'Should revert plugin permission (ZOKRATES)': function (browser) {
browser.waitForElementVisible('*[data-id="verticalIconsSettingsIcons"]')
.click('*[data-id="verticalIconsSettingsIcons"]')
.waitForElementVisible('*[data-id="pluginManagerPermissionsButton"]')
.click('*[data-id="pluginManagerPermissionsButton"]')
.waitForElementVisible('*[data-id="modalDialogContainer"]')
.click('*[data-id="pluginManagerSettingsPermissionForm"]')
.pause(2000)
.click('*[data-id="pluginManagerSettingsClearAllPermission"]')
.pause(2000)
.assert.containsText('*[data-id="pluginManagerSettingsPermissionForm"]', 'No Permission requested yet')
.modalFooterOKClick()
},
*/
'Should connect a local plugin': function (browser: NightwatchBrowser) { 'Should connect a local plugin': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="pluginManagerComponentPluginManager"]') browser.waitForElementVisible('*[data-id="pluginManagerComponentPluginManager"]')
.execute(function () { .execute(function () {
@ -130,8 +85,6 @@ module.exports = {
.click('*[data-id="localPluginRadioButtonsidePanel"]') .click('*[data-id="localPluginRadioButtonsidePanel"]')
.click('*[data-id="pluginManagerLocalPluginModalDialogModalDialogModalFooter-react"]') .click('*[data-id="pluginManagerLocalPluginModalDialogModalDialogModalFooter-react"]')
.click('*[data-id="pluginManagerLocalPluginModalDialog-modal-footer-ok-react') .click('*[data-id="pluginManagerLocalPluginModalDialog-modal-footer-ok-react')
// .modalFooterOKClick()
// .waitForElementVisible('*[data-id="pluginManagerComponentDeactivateButtonremixIde"]', 60000)
}, },
'Should display error message for creating already existing plugin': function (browser: NightwatchBrowser) { 'Should display error message for creating already existing plugin': function (browser: NightwatchBrowser) {
@ -154,46 +107,17 @@ module.exports = {
.assert.containsText('*[data-shared="tooltipPopup"]', 'Cannot create Plugin : This name has already been used') .assert.containsText('*[data-shared="tooltipPopup"]', 'Cannot create Plugin : This name has already been used')
}, },
'Local plugin should activate LearnEth plugin': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="pluginManagerComponentPluginManager"]')
.click('*[data-id="pluginManagerComponentPluginSearchButton"]')
.waitForElementVisible('*[data-id="pluginManagerLocalPluginModalDialogModalDialogContainer-react"]')
.click('*[data-id="pluginManagerLocalPluginModalDialogModalDialogModalBody-react"]')
.waitForElementVisible('*[data-id="localPluginName"]')
.clearValue('*[data-id="localPluginName"]').setValue('*[data-id="localPluginName"]', localPluginData.pluginName)
.clearValue('*[data-id="localPluginDisplayName"]').setValue('*[data-id="localPluginDisplayName"]', localPluginData.pluginDisplayName)
.clearValue('*[data-id="localPluginCanActivate"]').setValue('*[data-id="localPluginCanActivate"]', localPluginData.pluginCanActivate)
.clearValue('*[data-id="localPluginUrl"]').setValue('*[data-id="localPluginUrl"]', localPluginData.pluginUrl)
.click('*[data-id="localPluginRadioButtoniframe"]')
.click('*[data-id="localPluginRadioButtonsidePanel"]')
.click('*[data-id="pluginManagerLocalPluginModalDialogModalDialogModalFooter-react"]')
.click('*[data-id="pluginManagerLocalPluginModalDialog-modal-footer-ok-react')
.waitForElementVisible('[data-id="verticalIconsKindlocalPlugin"]')
.click('[data-id="verticalIconsKindlocalPlugin"]')
.waitForElementNotPresent('[data-id="verticalIconsKindLearnEth"]')
.pause(2000)
// @ts-ignore
.frame('plugin-localPlugin')
.useXpath().click("//button[text()='Activate Learneth']")
.pause(2000)
.frameParent()
.useCss().waitForElementPresent('[data-id="verticalIconsKindLearnEth"]')
},
'Should load back installed plugins after reload': function (browser: NightwatchBrowser) { 'Should load back installed plugins after reload': function (browser: NightwatchBrowser) {
browser browser
.waitForElementVisible('*[data-id="remixIdeSidePanel"]') .waitForElementVisible('*[data-id="remixIdeSidePanel"]')
.click('*[plugin="pluginManager"]')
.waitForElementVisible('*[data-id="pluginManagerComponentPluginManager"]') .waitForElementVisible('*[data-id="pluginManagerComponentPluginManager"]')
.getInstalledPlugins((plugins) => { .getInstalledPlugins((plugins) => {
browser.refresh() browser.refresh()
.waitForElementVisible('*[data-id="remixIdeSidePanel"]') .waitForElementVisible('*[data-id="remixIdeSidePanel"]')
.pause(3000) .pause(3000)
.perform((done) => { .perform((done) => {
// const filtered = plugins.filter(plugin => plugin !== 'testremixIde') // remove this when localplugin bug is resolved
plugins.forEach(plugin => { plugins.forEach(plugin => {
if ((plugin !== testData.pluginName) && plugin !== localPluginData.pluginName) { if (plugin !== testData.pluginName) {
browser.waitForElementVisible(`[plugin="${plugin}"`) browser.waitForElementVisible(`[plugin="${plugin}"`)
} }
}) })

@ -0,0 +1,327 @@
'use strict'
import { ExternalProfile, LocationProfile, Profile } from '@remixproject/plugin-utils'
import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init'
declare global {
interface Window { testmode: boolean; }
}
const localPluginData: Profile & LocationProfile & ExternalProfile = {
name: 'localPlugin',
displayName: 'Local Plugin',
canActivate: ['dGitProvider', 'flattener', 'solidityUnitTesting'],
url: 'http://localhost:2020',
location: 'sidePanel'
}
const getBrowserLogs = async function (browser: NightwatchBrowser) {
browser.getLog('browser', (logEntries) => {
if (logEntries && logEntries.length > 0) {
console.log('Browser log:')
console.log(logEntries)
}
})
}
const debugValues = async function (browser: NightwatchBrowser, field: string, expected: any) {
return new Promise((resolve) => {
if (!expected) {
resolve(true)
return
}
browser.waitForElementVisible(`//*[@id="${field}"]`).getText(`//*[@id="${field}"]`, (result) => {
console.log(result)
if (!result.value.toString().includes(expected)) {
console.log('Actual result:')
console.log(result.value.toString())
console.log('Expected result:')
console.log(expected)
getBrowserLogs(browser)
browser.assert.ok(false, 'Returned value from call does not match expected value.')
} else {
browser.assert.ok(true)
}
resolve(true)
})
})
}
const setPayload = async (browser: NightwatchBrowser, payload: any) => {
return new Promise((resolve) => {
if (typeof payload !== 'string') payload = JSON.stringify(payload)
browser.clearValue('//*[@id="payload"]').setValue('//*[@id="payload"]', payload, (result) => {
resolve(result)
})
})
}
const clearPayLoad = async (browser: NightwatchBrowser) => {
return new Promise((resolve) => {
browser.clearValue('//*[@id="payload"]', () => {
resolve(true)
})
})
}
const clickButton = async (browser: NightwatchBrowser, buttonText: string) => {
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))
})
})
}
const checkForAcceptAndRemember = async function (browser: NightwatchBrowser) {
return new Promise((resolve) => {
browser.frameParent(() => {
browser.pause(1000).element('xpath', '//*[@data-id="permissionHandlerRememberUnchecked"]', (visible:any) => {
if (visible.status && visible.status === -1) {
// @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"]', () => {
// @ts-ignore
browser.frame(0, () => { resolve(true) })
})
}
})
})
})
}
/**
* performs an action on the test local plugin calling a method on a plugin
*
* @param {NightwatchBrowser} browser
* @param {string} buttonText the button which needs to be clicked formatted as pluginname:methodname, ie 'fileManager:writeFile'
* @param {any} methodResult can be a string expected or an object. it is the result of the method called.
* @param {any} eventResult can be a string expected or an object. it is the event generated by the method called.
* @param {any} payload can be a string expected or an object. it is the payload passed to the call
* @return {Promise}
*/
const clickAndCheckLog = async (browser: NightwatchBrowser, buttonText: string, methodResult: any, eventResult: any, payload: any) => {
if (payload) {
await setPayload(browser, payload)
} else {
await clearPayLoad(browser)
}
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 debugValues(browser, 'methods', methodResult)
await debugValues(browser, 'events', eventResult)
}
const assertPluginIsActive = function (browser: NightwatchBrowser, id: string, shouldBeVisible: boolean) {
if (shouldBeVisible) {
browser.waitForElementVisible(`//*[@data-id="verticalIconsKind${id}"]`)
} else {
browser.waitForElementNotPresent(`//*[@data-id="verticalIconsKind${id}"]`)
}
}
module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done, 'http://127.0.0.1:8080', false)
},
afterEach: function (browser: NightwatchBrowser) {
browser.getLog('browser', (logEntries) => {
console.log(logEntries)
})
},
'Should connect a local plugin': function (browser: NightwatchBrowser) {
browser.addLocalPlugin(localPluginData)
// @ts-ignore
.frame(0).useXpath()
},
// UDAPP
'Should get accounts': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'udapp:getAccounts', '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', null, null)
},
// context menu item
'Should create context menu item': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:registerContextMenuItem', null, null, {
id: 'localPlugin',
name: 'testCommand',
label: 'testCommand',
type: [],
extension: ['.sol'],
path: [],
pattern: []
})
await browser.useXpath().frameParent(async () => {
browser.useCss().clickLaunchIcon('filePanel')
.waitForElementVisible('[data-id="treeViewLitreeViewItemcontracts"]').element('css selector', '[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]', (visible: any) => {
if (visible.status && visible.status === -1) {
browser.click('[data-id="treeViewLitreeViewItemcontracts"]')
}
})
.waitForElementVisible('[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]')
.rightClick('[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]').useXpath().waitForElementVisible('//*[@id="menuitemtestcommand"]').click('//*[@id="menuitemtestcommand"]', async () => {
// @ts-ignore
browser.click('//*[@data-id="verticalIconsKindlocalPlugin"]').frame(0, async () => {
await clickAndCheckLog(browser, null, { id: 'localPlugin', name: 'testCommand', label: 'testCommand', type: [], extension: ['.sol'], path: ['contracts/1_Storage.sol'], pattern: [] }, null, null)
})
})
})
},
// FILESYSTEM
'Should get current workspace': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:getCurrentWorkspace', { name: 'default_workspace', isLocalhost: false, absolutePath: '.workspaces/default_workspace' }, null, null)
},
'Should get current files': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'fileManager:readdir', { contracts: { isDirectory: true }, scripts: { isDirectory: true }, tests: { isDirectory: true }, 'README.txt': { isDirectory: false } }, null, '/')
},
'Should throw error on current file': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'fileManager:getCurrentFile', 'Error from IDE : Error: No such file or directory No file selected', null, null)
},
'Should open readme.txt': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'fileManager:open', null, { event: 'currentFileChanged', args: ['README.txt'] }, 'README.txt')
},
'Should have current file': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'fileManager:getCurrentFile', 'README.txt', null, null)
},
'Should create dir': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'fileManager:mkdir', null, null, 'testdir')
await clickAndCheckLog(browser, 'fileManager:readdir', 'testdir', null, '/')
},
'Should get file': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'fileManager:getFile', 'REMIX EXAMPLE PROJECT', null, 'README.txt')
},
'Should close all files': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'fileManager:closeAllFiles', null, { event: 'noFileSelected', args: [] }, null)
},
'Should switch to file': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'fileManager:switchFile', null, { event: 'currentFileChanged', args: ['contracts/1_Storage.sol'] }, 'contracts/1_Storage.sol')
await clickAndCheckLog(browser, 'fileManager:getCurrentFile', 'contracts/1_Storage.sol', null, null)
await clickAndCheckLog(browser, 'fileManager:switchFile', null, { event: 'currentFileChanged', args: ['README.txt'] }, 'README.txt')
await clickAndCheckLog(browser, 'fileManager:getCurrentFile', 'README.txt', null, null)
},
'Should write to file': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'fileManager:writeFile', null, { event: 'fileSaved', args: ['README.txt'] }, ['README.txt', 'test'])
await clickAndCheckLog(browser, 'fileManager:readFile', 'test', null, 'README.txt')
},
'Should set file': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'fileManager:setFile', null, { event: 'fileAdded', args: ['new.sol'] }, ['new.sol', 'test'])
await clickAndCheckLog(browser, 'fileManager:readFile', 'test', null, 'new.sol')
},
'Should write to new file': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'fileManager:writeFile', null, { event: 'fileAdded', args: ['testing.txt'] }, ['testing.txt', 'test'])
await clickAndCheckLog(browser, 'fileManager:readFile', 'test', null, 'testing.txt')
},
'Should rename file': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'fileManager:rename', null, null, ['testing.txt', 'testrename.txt'])
await clickAndCheckLog(browser, 'fileManager:readFile', 'test', null, 'testrename.txt')
},
'Should create empty workspace': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:createWorkspace', null, null, ['emptyworkspace', true])
await clickAndCheckLog(browser, 'filePanel:getCurrentWorkspace', { name: 'emptyworkspace', isLocalhost: false, absolutePath: '.workspaces/emptyworkspace' }, null, null)
await clickAndCheckLog(browser, 'fileManager:readdir', {}, null, '/')
},
'Should create workspace': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:createWorkspace', null, null, 'testspace')
await clickAndCheckLog(browser, 'filePanel:getCurrentWorkspace', { name: 'testspace', isLocalhost: false, absolutePath: '.workspaces/testspace' }, null, null)
await clickAndCheckLog(browser, 'fileManager:readdir', { contracts: { isDirectory: true }, scripts: { isDirectory: true }, tests: { isDirectory: true }, 'README.txt': { isDirectory: false } }, null, null)
},
'Should get all workspaces': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:getWorkspaces', ['default_workspace', 'emptyworkspace', 'testspace'], null, null)
},
'Should have set workspace event': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:createWorkspace', null, { event: 'setWorkspace', args: [{ name: 'newspace', isLocalhost: false }] }, 'newspace')
},
'Should have event when switching workspace': async function (browser: NightwatchBrowser) {
// @ts-ignore
browser.frameParent().useCss().clickLaunchIcon('filePanel').click('*[data-id="workspacesSelect"] option[value="default_workspace"]').useXpath().click('//*[@data-id="verticalIconsKindlocalPlugin"]').frame(0, async () => {
await clickAndCheckLog(browser, null, null, { event: 'setWorkspace', args: [{ name: 'default_workspace', isLocalhost: false }] }, null)
})
},
'Should rename workspace': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:renameWorkspace', null, null, ['default_workspace', 'renamed'])
await clickAndCheckLog(browser, 'filePanel:getWorkspaces', ['emptyworkspace', 'testspace', 'newspace', 'renamed'], null, null)
},
'Should delete workspace': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:deleteWorkspace', null, null, ['testspace'])
await clickAndCheckLog(browser, 'filePanel:getWorkspaces', ['emptyworkspace', 'newspace', 'renamed'], null, null)
},
// DGIT
'Should have changes on new workspace': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'filePanel:createWorkspace', null, null, 'dgit')
await clickAndCheckLog(browser, 'dGitProvider:status', [['README.txt', 0, 2, 0], ['contracts/1_Storage.sol', 0, 2, 0], ['contracts/2_Owner.sol', 0, 2, 0], ['contracts/3_Ballot.sol', 0, 2, 0], ['scripts/deploy_ethers.js', 0, 2, 0], ['scripts/deploy_web3.js', 0, 2, 0], ['tests/4_Ballot_test.sol', 0, 2, 0]], null, null)
},
'Should stage contract': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'dGitProvider:add', null, null, {
filepath: 'contracts/1_Storage.sol'
})
await clickAndCheckLog(browser, 'dGitProvider:status', [['README.txt', 0, 2, 0], ['contracts/1_Storage.sol', 0, 2, 2], ['contracts/2_Owner.sol', 0, 2, 0], ['contracts/3_Ballot.sol', 0, 2, 0], ['scripts/deploy_ethers.js', 0, 2, 0], ['scripts/deploy_web3.js', 0, 2, 0], ['tests/4_Ballot_test.sol', 0, 2, 0]], null, null)
},
'Should commit changes': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'dGitProvider:commit', null, null, { author: { name: 'Remix', email: 'Remix' }, message: 'commit-message' })
await clickAndCheckLog(browser, 'dGitProvider:log', 'commit-message', null, null)
},
'Should have git log': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'dGitProvider:log', 'commit-message', null, null)
},
'Should have branches': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'dGitProvider:branches', [{ name: 'main' }], null, null)
},
// resolver
'Should resolve url': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'contentImport:resolve', '# Remix Project', null, 'https://github.com/ethereum/remix-project/blob/master/README.md')
},
'Should resolve and save url': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'contentImport:resolveAndSave', '# Remix Project', { event: 'fileAdded', args: ['.deps/github/ethereum/remix-project/README.md'] }, 'https://github.com/ethereum/remix-project/blob/master/README.md')
},
// UNIT TESTING
'Should activate solidityUnitTesting': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'manager:activatePlugin', null, null, 'solidityUnitTesting')
browser.frameParent()
assertPluginIsActive(browser, 'solidityUnitTesting', true)
// @ts-ignore
browser.frame(0)
await clickAndCheckLog(browser, 'manager:isActive', true, null, 'solidityUnitTesting')
},
'Should test from path with solidityUnitTesting': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'solidityUnitTesting:testFromPath', '"totalPassing":2,"totalFailing":0', null, 'tests/4_Ballot_test.sol')
},
'Should deactivate solidityUnitTesting': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'manager:deactivatePlugin', null, null, 'solidityUnitTesting')
browser.frameParent()
assertPluginIsActive(browser, 'solidityUnitTesting', false)
// @ts-ignore
browser.frame(0)
await clickAndCheckLog(browser, 'manager:isActive', false, null, 'solidityUnitTesting')
},
// COMPILER
'Should compile a file': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'solidity:compile', null, null, 'contracts/1_Storage.sol')
browser.pause(5000, async () => {
await clickAndCheckLog(browser, 'solidity:compile', null, 'compilationFinished', null)
})
},
'Should get compilationresults': async function (browser: NightwatchBrowser) {
await clickAndCheckLog(browser, 'solidity:getCompilationResult', 'contracts/1_Storage.sol', null, null)
}
}

@ -129,7 +129,7 @@ contract t2est {
const records = `{ const records = `{
"accounts": { "accounts": {
"account{2}": "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c" "account{10}": "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c"
}, },
"linkReferences": { "linkReferences": {
"testLib": "created{1512830014773}" "testLib": "created{1512830014773}"
@ -146,7 +146,7 @@ const records = `{
"linkReferences": {}, "linkReferences": {},
"inputs": "()", "inputs": "()",
"type": "constructor", "type": "constructor",
"from": "account{2}" "from": "account{10}"
} }
}, },
{ {
@ -172,7 +172,7 @@ const records = `{
"name": "", "name": "",
"type": "constructor", "type": "constructor",
"inputs": "(uint256)", "inputs": "(uint256)",
"from": "account{2}" "from": "account{10}"
} }
}, },
{ {
@ -188,7 +188,7 @@ const records = `{
"name": "set", "name": "set",
"inputs": "(uint256,address)", "inputs": "(uint256,address)",
"type": "function", "type": "function",
"from": "account{2}" "from": "account{10}"
} }
} }
], ],
@ -287,7 +287,7 @@ const records = `{
const scenario = { const scenario = {
accounts: { accounts: {
'account{2}': '0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c' 'account{10}': '0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c'
}, },
linkReferences: {}, linkReferences: {},
transactions: [ transactions: [
@ -305,7 +305,7 @@ const scenario = {
name: '', name: '',
type: 'constructor', type: 'constructor',
inputs: '(uint256)', inputs: '(uint256)',
from: 'account{2}' from: 'account{10}'
} }
}, },
{ {
@ -320,7 +320,7 @@ const scenario = {
name: 'set', name: 'set',
inputs: '(uint256)', inputs: '(uint256)',
type: 'function', type: 'function',
from: 'account{2}' from: 'account{10}'
} }
} }
], ],

@ -85,7 +85,7 @@ module.exports = {
.waitForElementPresent('#staticanalysisresult .warning', 2000, true, function () { .waitForElementPresent('#staticanalysisresult .warning', 2000, true, function () {
browser browser
.click('[data-id="staticAnalysisModuleMiscellaneous1"') .click('[data-id="staticAnalysisModuleMiscellaneous1"')
.waitForElementPresent('.highlightLine15', 60000) .waitForElementPresent('.highlightLine16', 60000)
.getEditorValue((content) => { .getEditorValue((content) => {
browser.assert.ok(content.indexOf( browser.assert.ok(content.indexOf(
'function _sendLogPayload(bytes memory payload) private view {') !== -1, 'function _sendLogPayload(bytes memory payload) private view {') !== -1,
@ -153,7 +153,6 @@ function runTests (browser: NightwatchBrowser) {
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.waitForElementVisible('[data-path="folder1"]') .waitForElementVisible('[data-path="folder1"]')
.click('[data-path="folder1"]') .click('[data-path="folder1"]')
.click('[data-path="folder1"]') // click twice because remixd does not return nested folder details after update
.waitForElementVisible('[data-path="folder1/contract1.sol"]') .waitForElementVisible('[data-path="folder1/contract1.sol"]')
.waitForElementVisible('[data-path="folder1/renamed_contract_' + browserName + '.sol"]') // check if renamed file is preset .waitForElementVisible('[data-path="folder1/renamed_contract_' + browserName + '.sol"]') // check if renamed file is preset
.waitForElementNotPresent('[data-path="folder1/contract_' + browserName + '.sol"]') // check if renamed (old) file is not present .waitForElementNotPresent('[data-path="folder1/contract_' + browserName + '.sol"]') // check if renamed (old) file is not present

@ -75,7 +75,9 @@ module.exports = {
.waitForElementVisible('[data-id="https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/token/ERC20/ERC20.sol"]', 120000) .waitForElementVisible('[data-id="https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/token/ERC20/ERC20.sol"]', 120000)
.scrollAndClick('[data-id="https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/token/ERC20/ERC20.sol"]') // click on error which point to ERC20 code .scrollAndClick('[data-id="https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/token/ERC20/ERC20.sol"]') // click on error which point to ERC20 code
.pause(5000) .pause(5000)
.waitForElementContainsText('#input', 'contract ERC20 is Context, IERC20', 60000) .getEditorValue((content) => {
browser.assert.ok(content.indexOf('contract ERC20 is Context, IERC20') !== -1, 'content does not contain "contract ERC20 is Context, IERC20"')
})
}, },
'Test NPM Import (with unpkg.com)': function (browser: NightwatchBrowser) { 'Test NPM Import (with unpkg.com)': function (browser: NightwatchBrowser) {

@ -32,9 +32,10 @@ module.exports = {
.click('*[data-id="verticalIconsKindsolidityUnitTesting"]') .click('*[data-id="verticalIconsKindsolidityUnitTesting"]')
.waitForElementPresent('*[data-id="testTabGenerateTestFile"]') .waitForElementPresent('*[data-id="testTabGenerateTestFile"]')
.click('*[data-id="testTabGenerateTestFile"]') .click('*[data-id="testTabGenerateTestFile"]')
.waitForElementPresent('*[title="tests/simple_storage_test.sol"]') .waitForElementPresent('*[title="default_workspace/tests/simple_storage_test.sol"]')
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.pause(10000) .waitForElementPresent('[data-id="treeViewDivtreeViewItemtests"]')
.click('[data-id="treeViewDivtreeViewItemtests"]')
.openFile('tests/simple_storage_test.sol') .openFile('tests/simple_storage_test.sol')
.removeFile('tests/simple_storage_test.sol', 'default_workspace') .removeFile('tests/simple_storage_test.sol', 'default_workspace')
}, },
@ -93,7 +94,7 @@ module.exports = {
'Should fail on compilation, open file on error click, not disappear error': function (browser: NightwatchBrowser) { 'Should fail on compilation, open file on error click, not disappear error': function (browser: NightwatchBrowser) {
browser.waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]') browser.waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]')
.addFile('tests/compilationError_test.sol', sources[0]['compilationError_test.sol']) .addFile('tests/compilationError_test.sol', sources[0]['compilationError_test.sol'])
.click('div[title="default_workspace/tests/compilationError_test.sol"] span[class="close"]') .click('div[title="default_workspace/tests/compilationError_test.sol"] span[class="close-tabs"]')
.clickLaunchIcon('solidityUnitTesting') .clickLaunchIcon('solidityUnitTesting')
.pause(2000) .pause(2000)
.click('*[data-id="testTabCheckAllTests"]') .click('*[data-id="testTabCheckAllTests"]')
@ -101,12 +102,12 @@ module.exports = {
.scrollAndClick('*[data-id="testTabRunTestsTabRunAction"]') .scrollAndClick('*[data-id="testTabRunTestsTabRunAction"]')
.waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'SyntaxError: No visibility specified', 120000) .waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'SyntaxError: No visibility specified', 120000)
.waitForElementContainsText('*[data-id="testTabTestsExecutionStoppedError"]', 'The test execution has been stopped because of error(s) in your test file', 120000) .waitForElementContainsText('*[data-id="testTabTestsExecutionStoppedError"]', 'The test execution has been stopped because of error(s) in your test file', 120000)
.click('*[data-id="tests/compilationError_test.sol"]') .click('#solidityUnittestsOutput *[data-id="tests/compilationError_test.sol"]')
.pause(1000) .pause(1000)
.getEditorValue((content) => browser.assert.ok(content.indexOf('contract failOnCompilation {') !== -1)) .getEditorValue((content) => browser.assert.ok(content.indexOf('contract failOnCompilation {') !== -1))
// Verify that compilation error is still present after a file is opened // Verify that compilation error is still present after a file is opened
// usually, tests result is cleared on opening a new file // usually, tests result is cleared on opening a new file
.verify.elementPresent('*[data-id="tests/compilationError_test.sol"]') .verify.elementPresent('#solidityUnittestsOutput *[data-id="tests/compilationError_test.sol"]')
}, },
'Should fail on deploy': function (browser: NightwatchBrowser) { 'Should fail on deploy': function (browser: NightwatchBrowser) {
@ -164,8 +165,9 @@ module.exports = {
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_new' }) .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_new' })
.click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') .waitForElementVisible('*[data-id="fileSystem-modal-footer-ok-react"]')
.click('*[data-id="workspacesSelect"] option[value="workspace_new"]') .execute(function () { (document.querySelector('[data-id="fileSystem-modal-footer-ok-react"]') as HTMLElement).click() })
.waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_new"]')
// end of creating // end of creating
.clickLaunchIcon('solidityUnitTesting') .clickLaunchIcon('solidityUnitTesting')
.pause(2000) .pause(2000)
@ -181,6 +183,7 @@ module.exports = {
.clickLaunchIcon('solidityUnitTesting') .clickLaunchIcon('solidityUnitTesting')
.pause(2000) .pause(2000)
.verify.attributeEquals('*[data-id="uiPathInput"]', 'value', 'tests') .verify.attributeEquals('*[data-id="uiPathInput"]', 'value', 'tests')
.pause(2000)
.scrollAndClick('#runTestsTabRunAction') .scrollAndClick('#runTestsTabRunAction')
.waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000)
.waitForElementPresent('#solidityUnittestsOutput div[class^="testPass"]', 60000) .waitForElementPresent('#solidityUnittestsOutput div[class^="testPass"]', 60000)
@ -190,8 +193,17 @@ module.exports = {
}, },
'Solidity Unit tests with hardhat console log': function (browser: NightwatchBrowser) { 'Solidity Unit tests with hardhat console log': function (browser: NightwatchBrowser) {
const runtimeBrowser = browser.options.desiredCapabilities.browserName
browser browser
.waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]') .waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]')
.perform((done) => {
if (runtimeBrowser !== 'chrome') {
browser.clickLaunchIcon('filePanel')
.waitForElementVisible('[data-id="treeViewLitreeViewItemtests"]')
}
done()
})
.addFile('tests/hhLogs_test.sol', sources[0]['tests/hhLogs_test.sol']) .addFile('tests/hhLogs_test.sol', sources[0]['tests/hhLogs_test.sol'])
.clickLaunchIcon('solidityUnitTesting') .clickLaunchIcon('solidityUnitTesting')
.waitForElementVisible('*[id="singleTesttests/4_Ballot_test.sol"]', 60000) .waitForElementVisible('*[id="singleTesttests/4_Ballot_test.sol"]', 60000)
@ -201,13 +213,13 @@ module.exports = {
.waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000)
.waitForElementPresent('#solidityUnittestsOutput div[class^="testPass"]', 60000) .waitForElementPresent('#solidityUnittestsOutput div[class^="testPass"]', 60000)
.waitForElementContainsText('#solidityUnittestsOutput', 'tests/hhLogs_test.sol', 60000) .waitForElementContainsText('#solidityUnittestsOutput', 'tests/hhLogs_test.sol', 60000)
.assert.containsText('#journal > div:nth-child(3) > span > div', 'Before all:') .assert.containsText('#journal > div:nth-child(2) > span', 'Before all:')
.assert.containsText('#journal > div:nth-child(3) > span > div', 'Inside beforeAll') .assert.containsText('#journal > div:nth-child(2) > span', 'Inside beforeAll')
.assert.containsText('#journal > div:nth-child(4) > span > div', 'Check sender:') .assert.containsText('#journal > div:nth-child(3) > span', 'Check sender:')
.assert.containsText('#journal > div:nth-child(4) > span > div', 'msg.sender is 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4') .assert.containsText('#journal > div:nth-child(3) > span', 'msg.sender is 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4')
.assert.containsText('#journal > div:nth-child(5) > span > div', 'Check int logs:') .assert.containsText('#journal > div:nth-child(4) > span', 'Check int logs:')
.assert.containsText('#journal > div:nth-child(5) > span > div', '10 20') .assert.containsText('#journal > div:nth-child(4) > span', '10 20')
.assert.containsText('#journal > div:nth-child(5) > span > div', 'Number is 25') .assert.containsText('#journal > div:nth-child(4) > span', 'Number is 25')
.openFile('tests/hhLogs_test.sol') .openFile('tests/hhLogs_test.sol')
.removeFile('tests/hhLogs_test.sol', 'workspace_new') .removeFile('tests/hhLogs_test.sol', 'workspace_new')
}, },
@ -223,13 +235,13 @@ module.exports = {
.pause(2000) .pause(2000)
.waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000)
.waitForElementContainsText('#solidityUnittestsOutput', 'tests/ballotFailedLog_test.sol', 60000) .waitForElementContainsText('#solidityUnittestsOutput', 'tests/ballotFailedLog_test.sol', 60000)
.assert.containsText('#journal > div:nth-child(6) > span > div', 'Check winning proposal:') .assert.containsText('#journal > div:nth-child(5) > span', 'Check winning proposal:')
.assert.containsText('#journal > div:nth-child(6) > span > div', 'Inside checkWinningProposal') .assert.containsText('#journal > div:nth-child(5) > span', 'Inside checkWinningProposal')
.openFile('tests/ballotFailedLog_test.sol') .openFile('tests/ballotFailedLog_test.sol')
.removeFile('tests/ballotFailedLog_test.sol', 'workspace_new') .removeFile('tests/ballotFailedLog_test.sol', 'workspace_new')
}, },
'Debug failed test using debugger': function (browser: NightwatchBrowser) { 'Debug tests using debugger': function (browser: NightwatchBrowser) {
browser browser
.waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]') .waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]')
.addFile('tests/ballotFailedDebug_test.sol', sources[0]['tests/ballotFailedDebug_test.sol']) .addFile('tests/ballotFailedDebug_test.sol', sources[0]['tests/ballotFailedDebug_test.sol'])
@ -239,21 +251,50 @@ module.exports = {
.click('#runTestsTabRunAction') .click('#runTestsTabRunAction')
.waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000)
.waitForElementContainsText('#solidityUnittestsOutput', 'tests/ballotFailedDebug_test.sol', 60000) .waitForElementContainsText('#solidityUnittestsOutput', 'tests/ballotFailedDebug_test.sol', 60000)
.waitForElementContainsText('#solidityUnittestsOutput', '✘ Check winning proposal', 60000) .waitForElementContainsText('#solidityUnittestsOutput', '✘ Check winning proposal failed', 60000)
.waitForElementContainsText('#solidityUnittestsOutput', '✓ Check winning proposal passed', 60000)
.waitForElementContainsText('#solidityUnittestsOutput', '✘ Check winning proposal again', 60000)
.waitForElementContainsText('#solidityUnittestsOutput', '✓ Check winnin proposal with return value', 60000) .waitForElementContainsText('#solidityUnittestsOutput', '✓ Check winnin proposal with return value', 60000)
.click('.fa-bug') .click('#Check_winning_proposal_failed')
.waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000) .waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000)
.waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposal()', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalFailed()', 60000)
.click('*[data-id="dropdownPanelSolidityLocals"]') .click('*[data-id="dropdownPanelSolidityLocals"]')
.waitForElementContainsText('*[data-id="solidityLocals"]', 'no locals', 60000) .waitForElementContainsText('*[data-id="solidityLocals"]', 'no locals', 60000)
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
.execute(function () { document.getElementById('slider')['value'] = '235' }) // It only moves slider to 235 but vm traces are not updated .execute(function () { document.getElementById('slider')['value'] = '315' }) // It only moves slider to 315 but vm traces are not updated
.setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW)) .setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW))
.waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposal()', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalFailed()', 60000)
.waitForElementContainsText('*[data-id="functionPanel"]', 'vote(proposal)', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'vote(proposal)', 60000)
.pause(2000) .pause(1000)
// Should be uncommented while fixing https://github.com/ethereum/remix-project/issues/1644 .checkVariableDebug('soliditylocals', locals)
// .checkVariableDebug('soliditylocals', locals) .clickLaunchIcon('solidityUnitTesting')
.scrollAndClick('#Check_winning_proposal_passed')
.waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000)
.waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalPassed()', 60000)
// eslint-disable-next-line dot-notation
.execute(function () { document.getElementById('slider')['value'] = '1450' })
.setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW))
.waitForElementContainsText('*[data-id="functionPanel"]', 'equal(a, b, message)', 60000)
.waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalPassed()', 60000)
.pause(1000)
.clickLaunchIcon('solidityUnitTesting')
.scrollAndClick('#Check_winning_proposal_again')
.waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000)
.waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalAgain()', 60000)
// eslint-disable-next-line dot-notation
.execute(function () { document.getElementById('slider')['value'] = '1150' })
.setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW))
.waitForElementContainsText('*[data-id="functionPanel"]', 'equal(a, b, message)', 60000)
.waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalAgain()', 60000)
.pause(1000)
.clickLaunchIcon('solidityUnitTesting')
.scrollAndClick('#Check_winnin_proposal_with_return_value')
.waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000)
.waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinninProposalWithReturnValue()', 60000)
// eslint-disable-next-line dot-notation
.execute(function () { document.getElementById('slider')['value'] = '320' })
.setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW))
.waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinninProposalWithReturnValue()', 60000)
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.pause(2000) .pause(2000)
.openFile('tests/ballotFailedDebug_test.sol') .openFile('tests/ballotFailedDebug_test.sol')
@ -269,6 +310,7 @@ module.exports = {
.scrollAndClick('[data-id="pluginManagerComponentDeactivateButtonsolidityUnitTesting"]') .scrollAndClick('[data-id="pluginManagerComponentDeactivateButtonsolidityUnitTesting"]')
.pause(2000) .pause(2000)
.scrollAndClick('[data-id="pluginManagerComponentActivateButtonsolidityUnitTesting"]') .scrollAndClick('[data-id="pluginManagerComponentActivateButtonsolidityUnitTesting"]')
.pause(5000)
.clickLaunchIcon('solidityUnitTesting') .clickLaunchIcon('solidityUnitTesting')
.scrollAndClick('#runTestsTabRunAction') .scrollAndClick('#runTestsTabRunAction')
.waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000)
@ -458,7 +500,7 @@ const sources = [
}, },
'tests/deployError_test.sol': { 'tests/deployError_test.sol': {
content: ` content: `
pragma solidity ^0.7.0; pragma solidity ^0.8.0;
contract failingDeploy { contract failingDeploy {
constructor() { constructor() {
@ -469,7 +511,7 @@ const sources = [
}, },
'tests/methodFailure_test.sol': { 'tests/methodFailure_test.sol': {
content: ` content: `
pragma solidity ^0.7.0; pragma solidity ^0.8.0;
contract methodfailure { contract methodfailure {
function add(uint a, uint b) public { function add(uint a, uint b) public {
@ -496,11 +538,20 @@ const sources = [
ballotToTest = new Ballot(proposalNames); ballotToTest = new Ballot(proposalNames);
} }
function checkWinningProposal () public { function checkWinningProposalFailed () public {
ballotToTest.vote(1); // This will revert the transaction ballotToTest.vote(1);
Assert.equal(ballotToTest.winningProposal(), uint(0), "proposal at index 0 should be the winning proposal");
}
function checkWinningProposalPassed () public {
ballotToTest.vote(0);
Assert.equal(ballotToTest.winningProposal(), uint(0), "proposal at index 0 should be the winning proposal"); Assert.equal(ballotToTest.winningProposal(), uint(0), "proposal at index 0 should be the winning proposal");
} }
function checkWinningProposalAgain () public {
Assert.equal(ballotToTest.winningProposal(), uint(1), "proposal at index 0 should be the winning proposal");
}
function checkWinninProposalWithReturnValue () public view returns (bool) { function checkWinninProposalWithReturnValue () public view returns (bool) {
return ballotToTest.winningProposal() == 0; return ballotToTest.winningProposal() == 0;
} }
@ -558,7 +609,7 @@ const sources = [
} }
} }
] ]
/*
const locals = { const locals = {
sender: { sender: {
value: { value: {
@ -586,4 +637,3 @@ const locals = {
type: 'uint256' type: 'uint256'
} }
} }
*/

@ -11,6 +11,7 @@ module.exports = {
browser browser
.waitForElementVisible('*[data-id="terminalCli"]', 10000) .waitForElementVisible('*[data-id="terminalCli"]', 10000)
.executeScript('console.log(1 + 1)') .executeScript('console.log(1 + 1)')
.pause(2000)
.waitForElementContainsText('*[data-id="terminalJournal"]', '2', 60000) .waitForElementContainsText('*[data-id="terminalJournal"]', '2', 60000)
}, },
@ -30,24 +31,14 @@ module.exports = {
.assert.visible('*[data-id="autoCompletePopUpAutoCompleteItem"]') .assert.visible('*[data-id="autoCompletePopUpAutoCompleteItem"]')
}, },
'Should execute remix.help() command': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="terminalCli"]')
.executeScript('remix.help()')
.waitForElementContainsText('*[data-id="terminalJournal"]', 'remix.loadgist(id)', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', 'remix.loadurl(url)', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', 'remix.execute(filepath)', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', 'remix.exeCurrent()', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', 'remix.help()', 60000)
},
'Async/Await Script': function (browser: NightwatchBrowser) { 'Async/Await Script': function (browser: NightwatchBrowser) {
browser browser
.addFile('asyncAwait.js', { content: asyncAwait }) .addFile('asyncAwait.js', { content: asyncAwait })
.openFile('asyncAwait.js') .openFile('asyncAwait.js')
.executeScript('remix.execute(\'asyncAwait.js\')') .executeScript('remix.execute("asyncAwait.js")')
.waitForElementContainsText('*[data-id="terminalJournal"]', 'Waiting Promise', 60000) .waitForElementContainsText('*[data-id="terminalJournal"]', 'Waiting Promise', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', 'result - Promise Resolved', 60000) .waitForElementContainsText('*[data-id="terminalJournal"]', 'result - ', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', 'Promise Resolved', 60000)
}, },
'Call Remix File Manager from a script': function (browser: NightwatchBrowser) { 'Call Remix File Manager from a script': function (browser: NightwatchBrowser) {
@ -62,19 +53,19 @@ module.exports = {
'Call web3.eth.getAccounts() using JavaScript VM': function (browser: NightwatchBrowser) { 'Call web3.eth.getAccounts() using JavaScript VM': function (browser: NightwatchBrowser) {
browser browser
.executeScript('web3.eth.getAccounts()') .executeScript('web3.eth.getAccounts()')
.waitForElementContainsText('*[data-id="terminalJournal"]', '"0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c", "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db", "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB", "0x617F2E2fD72FD9D5503197092aC168c91465E7f2", "0x17F6AD8Ef982297579C203069C1DbfFE4348c372", "0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C"', 80000) .waitForElementContainsText('*[data-id="terminalJournal"]', '["0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2","0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db","0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB","0x617F2E2fD72FD9D5503197092aC168c91465E7f2","0x17F6AD8Ef982297579C203069C1DbfFE4348c372","0x5c6B0f7Bf3E7ce046039Bd8FABdfD3f9F5021678","0x03C6FcED478cBbC9a4FAB34eF9f40767739D1Ff7","0x1aE0EA34a72D944a8C7603FfB3eC30a6669E454C","0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC","0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c","0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C","0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB","0x583031D1113aD414F02576BD6afaBfb302140225","0xdD870fA1b7C4700F2BD7f44238821C26f7392148"]')
}, },
'Call web3.eth.getAccounts() using Web3 Provider': function (browser: NightwatchBrowser) { 'Call web3.eth.getAccounts() using Web3 Provider': function (browser: NightwatchBrowser) {
browser browser
.click('*[data-id="terminalClearConsole"]') // clear the terminal .click('*[data-id="terminalClearConsole"]') // clear the terminal
.clickLaunchIcon('udapp') .clickLaunchIcon('udapp')
.click('*[data-id="settingsWeb3Mode"]') .click('*[data-id="settingsWeb3Mode"]')
.modalFooterOKClick() .modalFooterOKClick()
.executeScript('web3.eth.getAccounts()') .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) // we check if an array is present, don't need to check for the content
.waitForElementContainsText('*[data-id="terminalJournal"]', '" ]', 60000) .waitForElementContainsText('*[data-id="terminalJournal"]', '"]', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', '", "', 60000) .waitForElementContainsText('*[data-id="terminalJournal"]', '","', 60000)
}, },
'Call Remix File Resolver (external URL) from a script': function (browser: NightwatchBrowser) { 'Call Remix File Resolver (external URL) from a script': function (browser: NightwatchBrowser) {
@ -120,19 +111,23 @@ module.exports = {
.addFile('deployWithEthersJs.js', { content: deployWithEthersJs }) .addFile('deployWithEthersJs.js', { content: deployWithEthersJs })
.openFile('deployWithEthersJs.js') .openFile('deployWithEthersJs.js')
.pause(1000) .pause(1000)
.click('[data-id="treeViewDivtreeViewItemcontracts"]')
.openFile('contracts/2_Owner.sol') .openFile('contracts/2_Owner.sol')
.clickLaunchIcon('solidity') .clickLaunchIcon('solidity')
.click('*[data-id="compilerContainerCompileBtn"]') // compile Owner .click('*[data-id="compilerContainerCompileBtn"]') // compile Owner
.executeScript('remix.execute(\'deployWithEthersJs.js\')') .executeScript('remix.execute(\'deployWithEthersJs.js\')')
.waitForElementContainsText('*[data-id="terminalJournal"]', 'Contract Address: 0xd9145CCE52D386f254917e481eB44e9943F39138', 60000) .waitForElementContainsText('*[data-id="terminalJournal"]', 'Contract Address:', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', '0xd9145CCE52D386f254917e481eB44e9943F39138', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', 'Deployment successful.', 60000) .waitForElementContainsText('*[data-id="terminalJournal"]', 'Deployment successful.', 60000)
.addAtAddressInstance('0xd9145CCE52D386f254917e481eB44e9943F39138', true, true) .addAtAddressInstance('0xd9145CCE52D386f254917e481eB44e9943F39138', true, true)
.click('*[data-id="terminalClearConsole"]') // clear the terminal .click('*[data-id="terminalClearConsole"]') // clear the terminal
.waitForElementPresent('*[data-id="universalDappUiContractActionWrapper"]', 60000) .waitForElementPresent('*[data-id="universalDappUiContractActionWrapper"]', 60000)
.click('*[data-id="universalDappUiTitleExpander"]') .click('*[data-id="universalDappUiTitleExpander"]')
.clickFunction('changeOwner - transact (not payable)', { types: 'address newOwner', values: '0xd9145CCE52D386f254917e481eB44e9943F39138' }) // execute the "changeOwner" function .clickFunction('changeOwner - transact (not payable)', { types: 'address newOwner', values: '0xd9145CCE52D386f254917e481eB44e9943F39138' }) // execute the "changeOwner" function
.waitForElementContainsText('*[data-id="terminalJournal"]', 'previousOwner0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', 60000) // check that the script is logging the event .waitForElementContainsText('*[data-id="terminalJournal"]', 'previousOwner', 60000) // check that the script is logging the event
.waitForElementContainsText('*[data-id="terminalJournal"]', 'newOwner0xd9145CCE52D386f254917e481eB44e9943F39138', 60000) .waitForElementContainsText('*[data-id="terminalJournal"]', '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', 60000) // check that the script is logging the event
.waitForElementContainsText('*[data-id="terminalJournal"]', 'newOwner', 60000)
.waitForElementContainsText('*[data-id="terminalJournal"]', '0xd9145CCE52D386f254917e481eB44e9943F39138', 60000)
.end() .end()
} }
} }

@ -99,7 +99,7 @@ module.exports = {
'decoded output': { 'decoded output': {
0: 'uint256: _uret 2343242', 0: 'uint256: _uret 2343242',
1: 'int256: _iret -4324324', 1: 'int256: _iret -4324324',
2: 'string: _strret string _ string _ string _ string _ string _ string _ string _ string _ string _ string _' 2: 'string: _strret string _ string _ string _ string _ string _ string _ string _ string _ string _ string _'
} }
}) })
.pause(500) .pause(500)
@ -147,12 +147,13 @@ module.exports = {
.waitForElementPresent('.instance:nth-of-type(3)') .waitForElementPresent('.instance:nth-of-type(3)')
.click('.instance:nth-of-type(3) > div > button') .click('.instance:nth-of-type(3) > div > button')
.clickFunction('g - transact (not payable)') .clickFunction('g - transact (not payable)')
.pause(5000)
.journalLastChildIncludes('Error provided by the contract:') .journalLastChildIncludes('Error provided by the contract:')
.journalLastChildIncludes('CustomError : error description') .journalLastChildIncludes('CustomError : error description')
.journalLastChildIncludes('Parameters:') .journalLastChildIncludes('Parameters:')
.journalLastChildIncludes('"value": "2",') .journalLastChildIncludes('"value": "2"')
.journalLastChildIncludes('"value": "3",') .journalLastChildIncludes('"value": "3"')
.journalLastChildIncludes('"value": "error_string_2",') .journalLastChildIncludes('"value": "error_string_2"')
.journalLastChildIncludes('"documentation": "param1"') .journalLastChildIncludes('"documentation": "param1"')
.journalLastChildIncludes('"documentation": "param2"') .journalLastChildIncludes('"documentation": "param2"')
.journalLastChildIncludes('"documentation": "param3"') .journalLastChildIncludes('"documentation": "param3"')
@ -170,9 +171,9 @@ module.exports = {
.journalLastChildIncludes('Error provided by the contract:') .journalLastChildIncludes('Error provided by the contract:')
.journalLastChildIncludes('CustomError : error description') .journalLastChildIncludes('CustomError : error description')
.journalLastChildIncludes('Parameters:') .journalLastChildIncludes('Parameters:')
.journalLastChildIncludes('"value": "2",') .journalLastChildIncludes('"value": "2"')
.journalLastChildIncludes('"value": "3",') .journalLastChildIncludes('"value": "3"')
.journalLastChildIncludes('"value": "error_string_2",') .journalLastChildIncludes('"value": "error_string_2"')
.journalLastChildIncludes('"documentation": "param1"') .journalLastChildIncludes('"documentation": "param1"')
.journalLastChildIncludes('"documentation": "param2"') .journalLastChildIncludes('"documentation": "param2"')
.journalLastChildIncludes('"documentation": "param3"') .journalLastChildIncludes('"documentation": "param3"')

@ -19,7 +19,7 @@ module.exports = {
browser browser
.pause(5000) .pause(5000)
.refresh() .refresh()
.pause(2000) .pause(10000)
.getEditorValue((content) => { .getEditorValue((content) => {
browser.assert.ok(content.indexOf('contract Ballot {') !== -1, 'content doesn\'t include Ballot contract') browser.assert.ok(content.indexOf('contract Ballot {') !== -1, 'content doesn\'t include Ballot contract')
}) })
@ -35,19 +35,26 @@ module.exports = {
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.click('*[data-id="workspaceCreate"]') // create workspace_name .click('*[data-id="workspaceCreate"]') // create workspace_name
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span')
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_name' }) .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_name' })
.click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.pause(1000)
.addFile('test.sol', { content: 'test' }) .addFile('test.sol', { content: 'test' })
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]')
.click('*[data-id="workspaceCreate"]') // create workspace_name_1 .click('*[data-id="workspaceCreate"]') // create workspace_name_1
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span')
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_name_1' }) .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_name_1' })
.click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.pause(2000)
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]')
.pause(2000)
.click('*[data-id="workspacesSelect"] option[value="workspace_name"]') .click('*[data-id="workspacesSelect"] option[value="workspace_name"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
}, },
@ -59,10 +66,15 @@ module.exports = {
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextRename"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextRename"]')
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextRename"]')['value'] = 'workspace_name_renamed' }) .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextRename"]')['value'] = 'workspace_name_renamed' })
.click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]')
.click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') .click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]')
.pause(2000)
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]')
.waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]')
.click('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]') .click('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]')
.pause(2000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]')
}, },
@ -70,8 +82,8 @@ module.exports = {
browser browser
.click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') .click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]')
.click('*[data-id="workspaceDelete"]') // delete workspace_name_1 .click('*[data-id="workspaceDelete"]') // delete workspace_name_1
.waitForElementVisible('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.waitForElementNotPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') .waitForElementNotPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]')
.end() .end()
}, },

@ -1,6 +1,6 @@
// Merge custom command types with nightwatch types // Merge custom command types with nightwatch types
/* eslint-disable no-use-before-define */
import { NightwatchBrowser } from 'nightwatch' import { NightwatchBrowser } from 'nightwatch' // eslint-disable-line @typescript-eslint/no-unused-vars
declare module 'nightwatch' { declare module 'nightwatch' {
export interface NightwatchCustomCommands { export interface NightwatchCustomCommands {
@ -27,9 +27,9 @@ declare module 'nightwatch' {
debugTransaction(index: number): NightwatchBrowser, debugTransaction(index: number): NightwatchBrowser,
checkElementStyle(cssSelector: string, styleProperty: string, expectedResult: string): NightwatchBrowser, checkElementStyle(cssSelector: string, styleProperty: string, expectedResult: string): NightwatchBrowser,
openFile(name: string): NightwatchBrowser, openFile(name: string): NightwatchBrowser,
editorScroll(direction: 'up' | 'down', numberOfTimes: number): NightwatchBrowser,
renamePath(path: string, newFileName: string, renamedPath: string): NightwatchBrowser, renamePath(path: string, newFileName: string, renamedPath: string): NightwatchBrowser,
rightClick(cssSelector: string): NightwatchBrowser, rightClick(cssSelector: string): NightwatchBrowser,
scrollToLine(line: number): NightwatchBrowser,
waitForElementContainsText(id: string, value: string, timeout?: number): NightwatchBrowser, waitForElementContainsText(id: string, value: string, timeout?: number): NightwatchBrowser,
getModalBody(callback: (value: string, cb: VoidFunction) => void): NightwatchBrowser, getModalBody(callback: (value: string, cb: VoidFunction) => void): NightwatchBrowser,
modalFooterCancelClick(): NightwatchBrowser, modalFooterCancelClick(): NightwatchBrowser,
@ -57,6 +57,8 @@ declare module 'nightwatch' {
checkAnnotationsNotPresent(type: string): NightwatchBrowser checkAnnotationsNotPresent(type: string): NightwatchBrowser
getLastTransactionHash(callback: (hash: string) => void) getLastTransactionHash(callback: (hash: string) => void)
currentWorkspaceIs(name: string): NightwatchBrowser currentWorkspaceIs(name: string): NightwatchBrowser
addLocalPlugin(this: NightwatchBrowser, profile: Profile & LocationProfile & ExternalProfile): NightwatchBrowser
acceptAndRemember (this: NightwatchBrowser, remember: boolean, accept: boolean): NightwatchBrowser
} }
export interface NightwatchBrowser { export interface NightwatchBrowser {

@ -12,7 +12,7 @@ npx nx serve remix-ide-e2e-src-local-plugin &
sleep 5 sleep 5
npm run build:e2e npm run build:e2e
npm run nightwatch_local_pluginManager || TEST_EXITCODE=1 npm run nightwatch_local_pluginApi || TEST_EXITCODE=1
echo "$TEST_EXITCODE" echo "$TEST_EXITCODE"
if [ "$TEST_EXITCODE" -eq 1 ] if [ "$TEST_EXITCODE" -eq 1 ]

@ -162,8 +162,7 @@ class App {
} }
init () { init () {
var self = this this.run().catch(console.error)
run.apply(self)
} }
render () { render () {
@ -202,317 +201,322 @@ class App {
` `
return self._view.el return self._view.el
} }
}
module.exports = App
async function run () { async run () {
var self = this 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 hosts = ['127.0.0.1:8080', '192.168.0.101:8080', 'localhost:8080'] // check the origin and warn message
// workaround for Electron support if (window.location.hostname === 'yann300.github.io') {
if (!isElectron() && !hosts.includes(window.location.host)) { modalDialogCustom.alert('This UNSTABLE ALPHA branch of Remix has been moved to http://ethereum.github.io/remix-live-alpha.')
// Oops! Accidentally trigger refresh or bookmark. } else if (window.location.hostname === 'remix-alpha.ethereum.org' ||
window.onbeforeunload = function () { (window.location.hostname === 'ethereum.github.io' && window.location.pathname.indexOf('/remix-live-alpha') === 0)) {
return 'Are you sure you want to leave?' 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.')
// APP_MANAGER
const appManager = self.appManager
const pluginLoader = appManager.pluginLoader
const workspace = pluginLoader.get()
const engine = new RemixEngine()
engine.register(appManager)
// SERVICES
// ----------------- 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)
})
// ----------------- editor service ----------------------------
const editor = new Editor({}, themeModule) // wrapper around ace editor
registry.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' })
// ----------------- dGit provider ---------------------------------
const dGitProvider = new DGitProvider()
// ----------------- import content service ------------------------
const contentImport = new CompilerImports()
const blockchain = new Blockchain(registry.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' })
// service which fetch contract artifacts from sourve-verify, put artifacts in remix and compile it
const fetchAndCompile = new FetchAndCompile()
// ----------------- network service (resolve network id / name) -----
const networkModule = new NetworkModule(blockchain)
// ----------------- represent the current selected web3 provider ----
const web3Provider = new Web3ProviderModule(blockchain)
const hardhatProvider = new HardhatProvider(blockchain)
// ----------------- convert offset to line/column service -----------
const offsetToLineColumnConverter = new OffsetToLineColumnConverter()
registry.put({ api: offsetToLineColumnConverter, name: 'offsettolinecolumnconverter' })
// -------------------Terminal----------------------------------------
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
return height - newpos
}
} }
)
makeUdapp(blockchain, compilersArtefacts, (domEl) => terminal.logHtml(domEl))
const contextualListener = new ContextualListener({ editor })
engine.register([
blockchain,
contentImport,
themeModule,
editor,
fileManager,
compilerMetadataGenerator,
compilersArtefacts,
networkModule,
offsetToLineColumnConverter,
contextualListener,
terminal,
web3Provider,
fetchAndCompile,
dGitProvider,
hardhatProvider
])
// 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
])
// 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)
const filePanel = new FilePanel(appManager)
const landingPage = new LandingPage(appManager, menuicons, fileManager, filePanel, contentImport)
const settings = new SettingsTab(
registry.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,
landingPage,
hiddenPanel,
sidePanel,
filePanel,
pluginManagerComponent,
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 hosts = ['127.0.0.1:8080', '192.168.0.101:8080', 'localhost:8080']
const walkthroughService = new WalkthroughService(localStorage) // workaround for Electron support
if (!params.code && !params.url && !params.minimizeterminal && !params.gist && !params.minimizesidepanel) { if (!isElectron() && !hosts.includes(window.location.host)) {
walkthroughService.start() // Oops! Accidentally trigger refresh or bookmark.
window.onbeforeunload = function () {
return 'Are you sure you want to leave?'
}
} }
}
// Ask to opt in to Matomo for remix, remix-alpha and remix-beta // APP_MANAGER
const matomoDomains = { const appManager = self.appManager
'remix-alpha.ethereum.org': 27, const pluginLoader = appManager.pluginLoader
'remix-beta.ethereum.org': 25, const workspace = pluginLoader.get()
'remix.ethereum.org': 23 const engine = new RemixEngine()
} engine.register(appManager)
if (matomoDomains[window.location.hostname] && !registry.get('config').api.exists('settings/matomo-analytics')) {
modalDialog( // SERVICES
'Help us to improve Remix IDE', // ----------------- theme service ---------------------------------
yo` const themeModule = new ThemeModule(registry)
<div> registry.put({ api: themeModule, name: 'themeModule' })
<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> themeModule.initTheme(() => {
<p>We realize that our users have sensitive information in their code and that their privacy - your privacy - must be protected.</p> setTimeout(() => {
<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> document.body.removeChild(self._view.splashScreen)
<p>We do not collect nor store any personally identifiable information (PII).</p> self._view.el.style.visibility = 'visible'
<p>For more info, see: <a href="https://medium.com/p/66ef69e14931/" target="_blank">Matomo Analyitcs on Remix iDE</a>.</p> }, 1500)
<p>You can change your choice in the Settings panel anytime.</p> })
<div class="d-flex justify-content-around pt-3 border-top"> // ----------------- editor service ----------------------------
<button class="btn btn-primary ${css.matomoBtn}" onclick=${() => onAcceptMatomo()}>Sure</button> const editor = new Editor() // wrapper around ace editor
<button class="btn btn-secondary ${css.matomoBtn}" onclick=${() => onDeclineMatomo()}>Decline</button> registry.put({ api: editor, name: 'editor' })
</div> editor.event.register('requiringToSaveCurrentfile', () => fileManager.saveCurrentFile())
</div>`,
{ // ----------------- fileManager service ----------------------------
label: '', const fileManager = new FileManager(editor, appManager)
fn: null registry.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)
// ----------------- 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' })
// service which fetch contract artifacts from sourve-verify, put artifacts in remix and compile it
const fetchAndCompile = new FetchAndCompile()
// ----------------- network service (resolve network id / name) -----
const networkModule = new NetworkModule(blockchain)
// ----------------- represent the current selected web3 provider ----
const web3Provider = new Web3ProviderModule(blockchain)
const hardhatProvider = new HardhatProvider(blockchain)
// ----------------- convert offset to line/column service -----------
const offsetToLineColumnConverter = new OffsetToLineColumnConverter()
registry.put({ api: offsetToLineColumnConverter, name: 'offsettolinecolumnconverter' })
// -------------------Terminal----------------------------------------
makeUdapp(blockchain, compilersArtefacts, (domEl) => terminal.logHtml(domEl))
const terminal = new Terminal(
{ appManager, blockchain },
{ {
label: '', getPosition: (event) => {
fn: null 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
return height - newpos
}
} }
) )
} else { const contextualListener = new ContextualListener({ editor })
startWalkthroughService()
} engine.register([
blockchain,
contentImport,
themeModule,
editor,
fileManager,
compilerMetadataGenerator,
compilersArtefacts,
networkModule,
offsetToLineColumnConverter,
contextualListener,
terminal,
web3Provider,
fetchAndCompile,
dGitProvider,
hardhatProvider
])
// 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
])
// 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)
const filePanel = new FilePanel(appManager)
const landingPage = new LandingPage(appManager, menuicons, fileManager, filePanel, contentImport)
const settings = new SettingsTab(
registry.get('config').api,
editor,
appManager
)
// CONTENT VIEWS & DEFAULT PLUGINS // adding Views to the DOM
const compileTab = new CompileTab(registry.get('config').api, registry.get('filemanager').api) self._view.mainpanel.appendChild(mainview.render())
const run = new RunTab( self._view.iconpanel.appendChild(menuicons.render())
blockchain, self._view.sidepanel.appendChild(sidePanel.render())
registry.get('config').api, document.body.appendChild(hiddenPanel.render()) // Hidden Panel is display none, it can be directly on body
registry.get('filemanager').api,
registry.get('editor').api, engine.register([
filePanel, menuicons,
registry.get('compilersartefacts').api, landingPage,
networkModule, hiddenPanel,
mainview, sidePanel,
registry.get('fileproviders/browser').api filePanel,
) pluginManagerComponent,
const analysis = new AnalysisTab(registry) settings
const debug = new DebuggerTab() ])
const test = new TestTab(
registry.get('filemanager').api, const queryParams = new QueryParams()
registry.get('offsettolinecolumnconverter').api, const params = queryParams.get()
filePanel,
compileTab, const onAcceptMatomo = () => {
appManager, _paq.push(['forgetUserOptOut'])
contentImport // @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)
engine.register([ const el = document.getElementById('modal-dialog')
compileTab, el.parentElement.removeChild(el)
run, startWalkthroughService()
debug, }
analysis, const onDeclineMatomo = () => {
test, settings.updateMatomoAnalyticsChoice(false)
filePanel.remixdHandle, _paq.push(['optUserOut'])
filePanel.gitHandle, const el = document.getElementById('modal-dialog')
filePanel.hardhatHandle, el.parentElement.removeChild(el)
filePanel.slitherHandle startWalkthroughService()
]) }
if (isElectron()) {
appManager.activatePlugin('remixd')
}
try { const startWalkthroughService = () => {
engine.register(await appManager.registeredPlugins()) const walkthroughService = new WalkthroughService(localStorage)
} catch (e) { if (!params.code && !params.url && !params.minimizeterminal && !params.gist && !params.minimizesidepanel) {
console.log('couldn\'t register iframe plugins', e.message) walkthroughService.start()
} }
}
await appManager.activatePlugin(['theme', 'editor', 'fileManager', 'compilerMetadata', 'compilerArtefacts', 'network', 'web3Provider', 'offsetToLineColumnConverter']) // Ask to opt in to Matomo for remix, remix-alpha and remix-beta
await appManager.activatePlugin(['mainPanel', 'menuicons', 'tabs']) const matomoDomains = {
await appManager.activatePlugin(['sidePanel']) // activating host plugin separately 'remix-alpha.ethereum.org': 27,
await appManager.activatePlugin(['home']) 'remix-beta.ethereum.org': 25,
await appManager.activatePlugin(['settings']) 'remix.ethereum.org': 23
await appManager.activatePlugin(['hiddenPanel', 'filePanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport']) }
await appManager.registerContextMenuItems() if (matomoDomains[window.location.hostname] && !registry.get('config').api.exists('settings/matomo-analytics')) {
// Set workspace after initial activation modalDialog(
if (Array.isArray(workspace)) { 'Help us to improve Remix IDE',
appManager.activatePlugin(workspace).then(async () => { yo`
try { <div>
if (params.deactivate) { <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>
await appManager.deactivatePlugin(params.deactivate.split(',')) <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
} }
} catch (e) { )
console.log(e) } else {
} startWalkthroughService()
}
if (params.code) { // CONTENT VIEWS & DEFAULT PLUGINS
// if code is given in url we focus on solidity plugin const compileTab = new CompileTab(registry.get('config').api, registry.get('filemanager').api)
menuicons.select('solidity') const run = new RunTab(
} else { blockchain,
// If plugins are loaded from the URL params, we focus on the last one. registry.get('config').api,
if (pluginLoader.current === 'queryParams' && workspace.length > 0) menuicons.select(workspace[workspace.length - 1]) registry.get('filemanager').api,
} registry.get('editor').api,
filePanel,
registry.get('compilersartefacts').api,
networkModule,
mainview,
registry.get('fileproviders/browser').api
)
const analysis = new AnalysisTab(registry)
const debug = new DebuggerTab()
const test = new TestTab(
registry.get('filemanager').api,
registry.get('offsettolinecolumnconverter').api,
filePanel,
compileTab,
appManager,
contentImport
)
if (params.call) { engine.register([
const callDetails = params.call.split('//') compileTab,
if (callDetails.length > 1) { run,
toolTip(`initiating ${callDetails[0]} ...`) debug,
// @todo(remove the timeout when activatePlugin is on 0.3.0) analysis,
appManager.call(...callDetails).catch(console.error) test,
} filePanel.remixdHandle,
filePanel.gitHandle,
filePanel.hardhatHandle,
filePanel.slitherHandle
])
if (isElectron()) {
appManager.activatePlugin('remixd')
}
try {
engine.register(await appManager.registeredPlugins())
} catch (e) {
console.log('couldn\'t register iframe plugins', e.message)
}
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'])
// 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(','))
}
} 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)
}
}
}).catch(console.error)
} else {
// activate solidity plugin
appManager.activatePlugin(['solidity', 'udapp'])
} }
}).catch(console.error) })
} else {
// activate solidity plugin
appManager.activatePlugin(['solidity', 'udapp'])
}
// Load and start the service who manager layout and frame // Load and start the service who manager layout and frame
const framingService = new FramingService(sidePanel, menuicons, mainview, this._components.resizeFeature) const framingService = new FramingService(sidePanel, menuicons, mainview, this._components.resizeFeature)
if (params.embed) framingService.embed() if (params.embed) framingService.embed()
framingService.start(params) framingService.start(params)
}
} }
module.exports = App

@ -31,7 +31,7 @@ export class MainPanel extends AbstractPanel {
render () { render () {
return yo` return yo`
<div class=${css.pluginsContainer} data-id="mainPanelPluginsContainer"> <div class=${css.pluginsContainer} data-id="mainPanelPluginsContainer" id='mainPanelPluginsContainer-id'>
${this.view} ${this.view}
</div>` </div>`
} }

@ -1,84 +0,0 @@
'use strict'
const SourceHighlighter = require('./sourceHighlighter')
class SourceHighlighters {
constructor () {
this.highlighters = {}
}
highlight (position, filePath, hexColor, from) {
// eslint-disable-next-line
try {
if (!this.highlighters[from]) this.highlighters[from] = []
const sourceHighlight = new SourceHighlighter()
if (
!this.highlighters[from].length ||
(this.highlighters[from].length && !this.highlighters[from].find((el) => {
return el.source === filePath && el.position === position
}))
) {
sourceHighlight.currentSourceLocationFromfileName(position, filePath, hexColor)
this.highlighters[from].push(sourceHighlight)
}
} catch (e) {
throw e
}
}
// highlights all locations for @from plugin
highlightAllFrom (from) {
// eslint-disable-next-line
try {
if (!this.highlighters[from]) return
let sourceHighlight
for (const index in this.highlighters[from]) {
sourceHighlight = new SourceHighlighter()
sourceHighlight.currentSourceLocationFromfileName(
this.highlighters[from][index].position,
this.highlighters[from][index].source,
this.highlighters[from][index].style
)
this.highlighters[from][index] = sourceHighlight
}
} catch (e) {
throw e
}
}
discardHighlight (from) {
if (this.highlighters[from]) {
for (const index in this.highlighters[from]) this.highlighters[from][index].currentSourceLocation(null)
}
this.highlighters[from] = []
}
discardAllHighlights () {
for (const from in this.highlighters) {
this.discardHighlight(from)
}
}
hideHighlightsExcept (toStay) {
for (const highlighter in this.highlighters) {
for (const index in this.highlighters[highlighter]) {
this.highlighters[highlighter][index].currentSourceLocation(null)
}
}
this.highlightAllFrom(toStay)
}
discardHighlightAt (line, filePath, from) {
if (this.highlighters[from]) {
for (const index in this.highlighters[from]) {
const highlight = this.highlighters[from][index]
if (highlight.source === filePath &&
(highlight.position.start.line === line || highlight.position.end.line === line)) {
highlight.currentSourceLocation(null)
this.highlighters[from].splice(index, 1)
}
}
}
}
}
module.exports = SourceHighlighters

@ -4,7 +4,6 @@ import * as packageJson from '../../../../../package.json'
import { sourceMappingDecoder } from '@remix-project/remix-debug' import { sourceMappingDecoder } from '@remix-project/remix-debug'
const { AstWalker } = require('@remix-project/remix-astwalker') const { AstWalker } = require('@remix-project/remix-astwalker')
const csjs = require('csjs-inject')
const EventManager = require('../../lib/events') const EventManager = require('../../lib/events')
const globalRegistry = require('../../global/registry') const globalRegistry = require('../../global/registry')
@ -127,14 +126,6 @@ class ContextualListener extends Plugin {
const lastCompilationResult = this._deps.compilersArtefacts.__last const lastCompilationResult = this._deps.compilersArtefacts.__last
if (lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0) { if (lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0) {
let lineColumn = this._deps.offsetToLineColumnConverter.offsetToLineColumn(position, position.file, lastCompilationResult.getSourceCode().sources, lastCompilationResult.getAsts()) let lineColumn = this._deps.offsetToLineColumnConverter.offsetToLineColumn(position, position.file, lastCompilationResult.getSourceCode().sources, lastCompilationResult.getAsts())
const css = csjs`
.highlightref_fullLine {
position: absolute;
z-index: 2;
opacity: 0.1;
background-color: var(--info);
}
`
if (node.nodes && node.nodes.length) { if (node.nodes && node.nodes.length) {
// If node has children, highlight the entire line. if not, just highlight the current source position of the node. // If node has children, highlight the entire line. if not, just highlight the current source position of the node.
lineColumn = { lineColumn = {
@ -150,7 +141,7 @@ class ContextualListener extends Plugin {
} }
const fileName = lastCompilationResult.getSourceName(position.file) const fileName = lastCompilationResult.getSourceName(position.file)
if (fileName) { if (fileName) {
return this.editor.addMarker(lineColumn, fileName, css.highlightref_fullLine) return this.call('editor', 'highlight', lineColumn, fileName, '', { focus: false })
} }
} }
return null return null
@ -178,10 +169,7 @@ class ContextualListener extends Plugin {
} }
_stopHighlighting () { _stopHighlighting () {
for (const eventKey in this._activeHighlights) { this.call('editor', 'discardHighlight')
const event = this._activeHighlights[eventKey]
this.editor.removeMarker(event.eventId, event.fileTarget)
}
this.event.trigger('stopHighlighting', []) this.event.trigger('stopHighlighting', [])
this._activeHighlights = [] this._activeHighlights = []
} }

@ -1,200 +1,95 @@
'use strict' 'use strict'
import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
import { EditorUI } from '@remix-ui/editor' // eslint-disable-line
import { Plugin } from '@remixproject/engine' import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
const EventManager = require('../../lib/events') const EventManager = require('../../lib/events')
const yo = require('yo-yo')
const csjs = require('csjs-inject')
const ace = require('brace')
const globalRegistry = require('../../global/registry')
const SourceHighlighters = require('./SourceHighlighters')
const Range = ace.acequire('ace/range').Range
require('brace/ext/language_tools')
require('brace/ext/searchbox')
const langTools = ace.acequire('ace/ext/language_tools')
require('ace-mode-solidity/build/remix-ide/mode-solidity')
require('ace-mode-move/build/remix-ide/mode-move')
require('ace-mode-zokrates')
require('ace-mode-lexon')
require('brace/mode/javascript')
require('brace/mode/python')
require('brace/mode/json')
require('brace/mode/rust')
require('brace/theme/chrome') // for all light themes
require('brace/theme/chaos') // for all dark themes
require('../../assets/js/editor/darkTheme') // a custom one for remix 'Dark' theme
const css = csjs`
.ace-editor {
width : 100%;
}
`
document.head.appendChild(yo`
<style>
.ace-tm .ace_gutter,
.ace-tm .ace_gutter-active-line,
.ace-tm .ace_marker-layer .ace_active-line {
background-color: var(--secondary);
}
.ace_gutter-cell.ace_breakpoint{
background-color: var(--secondary);
}
</style>
`)
const profile = { const profile = {
displayName: 'Editor', displayName: 'Editor',
name: 'editor', name: 'editor',
description: 'service - editor', description: 'service - editor',
version: packageJson.version, version: packageJson.version,
methods: ['highlight', 'discardHighlight', 'discardHighlightAt', 'clearAnnotations', 'addAnnotation', 'gotoLine'] methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addAnnotation', 'gotoLine']
} }
class Editor extends Plugin { class Editor extends Plugin {
constructor (opts = {}, themeModule) { constructor () {
super(profile) super(profile)
// Dependancies
this._components = {}
this._components.registry = globalRegistry
this._deps = {
config: this._components.registry.get('config').api
}
this._themes = { this._themes = {
light: 'chrome', light: 'light',
dark: 'chaos', dark: 'vs-dark',
remixDark: 'remixDark' remixDark: 'remix-dark'
} }
themeModule.events.on('themeChanged', (theme) => {
this.setTheme(theme.name === 'Dark' ? 'remixDark' : theme.quality)
})
// Init // Init
this.event = new EventManager() this.event = new EventManager()
this.sessions = {} this.sessions = {}
this.sourceAnnotationsPerFile = [] this.sourceAnnotationsPerFile = {}
this.markerPerFile = {}
this.readOnlySessions = {} this.readOnlySessions = {}
this.previousInput = '' this.previousInput = ''
this.saveTimeout = null this.saveTimeout = null
this.sourceHighlighters = new SourceHighlighters() this.emptySession = null
this.emptySession = this._createSession('')
this.modes = { this.modes = {
sol: 'ace/mode/solidity', sol: 'sol',
yul: 'ace/mode/solidity', yul: 'sol',
mvir: 'ace/mode/move', mvir: 'move',
js: 'ace/mode/javascript', js: 'javascript',
py: 'ace/mode/python', py: 'python',
vy: 'ace/mode/python', vy: 'python',
zok: 'ace/mode/zokrates', zok: 'zokrates',
lex: 'ace/mode/lexon', lex: 'lexon',
txt: 'ace/mode/text', txt: 'text',
json: 'ace/mode/json', json: 'json',
abi: 'ace/mode/json', abi: 'json',
rs: 'ace/mode/rust' rs: 'rust'
} }
// Editor Setup this.activated = false
const el = yo`<div id="input" data-id="editorInput"></div>`
this.editor = ace.edit(el)
ace.acequire('ace/ext/language_tools')
// Unmap ctrl-l & cmd-l
this.editor.commands.bindKeys({
'ctrl-L': null,
'Command-L': null
})
// shortcuts for "Ctrl-"" and "Ctrl+"" to increase/decrease font size of the editor
this.editor.commands.addCommand({
name: 'increasefontsizeEqual',
bindKey: { win: 'Ctrl-=', mac: 'Command-=' },
exec: (editor) => {
this.editorFontSize(1)
},
readOnly: true
})
this.editor.commands.addCommand({
name: 'increasefontsizePlus',
bindKey: { win: 'Ctrl-+', mac: 'Command-+' },
exec: (editor) => {
this.editorFontSize(1)
},
readOnly: true
})
this.editor.commands.addCommand({
name: 'decreasefontsize',
bindKey: { win: 'Ctrl--', mac: 'Command--' },
exec: (editor) => {
this.editorFontSize(-1)
},
readOnly: true
})
this.editor.setShowPrintMargin(false)
this.editor.resize(true)
this.editor.setOptions({
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
})
el.className += ' ' + css['ace-editor']
el.editor = this.editor // required to access the editor during tests
this.render = () => el
// Completer for editor this.events = {
const flowCompleter = { onBreakPointAdded: (file, line) => this.triggerEvent('breakpointAdded', [file, line]),
getCompletions: (editor, session, pos, prefix, callback) => { onBreakPointCleared: (file, line) => this.triggerEvent('breakpointCleared', [file, line]),
// @TODO add here other propositions onDidChangeContent: (file) => this._onChange(file),
} onEditorMounted: () => this.triggerEvent('editorMounted', [])
} }
langTools.addCompleter(flowCompleter)
// zoom with Ctrl+wheel // to be implemented by the react component
window.addEventListener('wheel', (e) => { this.api = {}
if (e.ctrlKey && Math.abs(e.wheelY) > 5) { }
this.editorFontSize(e.wheelY > 0 ? 1 : -1)
}
})
// EVENTS LISTENERS render () {
if (this.el) return this.el
// Gutter Mouse down this.el = document.createElement('div')
this.editor.on('guttermousedown', e => { this.el.setAttribute('id', 'editorView')
const target = e.domEvent.target this.el.currentContent = () => this.currentContent() // used by e2e test
if (target.className.indexOf('ace_gutter-cell') === -1) { this.el.setCurrentContent = (value) => {
return if (this.sessions[this.currentFile]) {
} this.sessions[this.currentFile].setValue(value)
const row = e.getDocumentPosition().row this._onChange(this.currentFile)
const breakpoints = e.editor.session.getBreakpoints()
for (const k in breakpoints) {
if (k === row.toString()) {
this.triggerEvent('breakpointCleared', [this.currentSession, row])
e.editor.session.clearBreakpoint(row)
e.stop()
return
}
} }
this.setBreakpoint(row) }
this.triggerEvent('breakpointAdded', [this.currentSession, row]) this.el.gotoLine = (line) => this.gotoLine(line, 0)
e.stop() return this.el
}) }
// Do setup on initialisation here renderComponent () {
this.editor.on('changeSession', () => { ReactDOM.render(
this._onChange() <EditorUI
this.triggerEvent('sessionSwitched', []) editorAPI={this.api}
this.editor.getSession().on('change', () => { theme={this.currentTheme}
this._onChange() currentFile={this.currentFile}
this.sourceHighlighters.discardAllHighlights() sourceAnnotationsPerFile={this.sourceAnnotationsPerFile}
this.triggerEvent('contentChanged', []) markerPerFile={this.markerPerFile}
}) events={this.events}
}) plugin={this}
/>
, this.el)
} }
triggerEvent (name, params) { triggerEvent (name, params) {
@ -203,14 +98,25 @@ class Editor extends Plugin {
} }
onActivation () { onActivation () {
this.activated = true
this.on('sidePanel', 'focusChanged', (name) => { this.on('sidePanel', 'focusChanged', (name) => {
this.sourceHighlighters.hideHighlightsExcept(name) this.keepDecorationsFor(name, 'sourceAnnotationsPerFile')
this.keepAnnotationsFor(name) this.keepDecorationsFor(name, 'markerPerFile')
}) })
this.on('sidePanel', 'pluginDisabled', (name) => { this.on('sidePanel', 'pluginDisabled', (name) => {
this.sourceHighlighters.discardHighlight(name) this.clearAllDecorationsFor(name)
this.clearAllAnnotationsFor(name) })
const translateTheme = (theme) => this._themes[theme.name === 'Dark' ? 'remixDark' : theme.quality]
this.on('theme', 'themeChanged', (theme) => {
this.currentTheme = translateTheme(theme)
this.renderComponent()
})
this.call('theme', 'currentTheme', (theme) => {
this.currentTheme = translateTheme(theme)
this.renderComponent()
}) })
this.renderComponent()
} }
onDeactivation () { onDeactivation () {
@ -218,30 +124,14 @@ class Editor extends Plugin {
this.off('sidePanel', 'pluginDisabled') this.off('sidePanel', 'pluginDisabled')
} }
highlight (position, filePath, hexColor) { async _onChange (file) {
const { from } = this.currentRequest const currentFile = await this.call('fileManager', 'file')
this.sourceHighlighters.highlight(position, filePath, hexColor, from)
}
discardHighlight () {
const { from } = this.currentRequest
this.sourceHighlighters.discardHighlight(from)
}
discardHighlightAt (line, filePath) {
const { from } = this.currentRequest
this.sourceHighlighters.discardHighlightAt(line, filePath, from)
}
setTheme (type) {
this.editor.setTheme('ace/theme/' + this._themes[type])
}
_onChange () {
const currentFile = this._deps.config.get('currentFile')
if (!currentFile) { if (!currentFile) {
return return
} }
if (currentFile !== file) {
return
}
const input = this.get(currentFile) const input = this.get(currentFile)
if (!input) { if (!input) {
return return
@ -257,16 +147,17 @@ class Editor extends Plugin {
if (this.saveTimeout) { if (this.saveTimeout) {
window.clearTimeout(this.saveTimeout) window.clearTimeout(this.saveTimeout)
} }
this.triggerEvent('contentChanged', [])
this.saveTimeout = window.setTimeout(() => { this.saveTimeout = window.setTimeout(() => {
this.triggerEvent('requiringToSaveCurrentfile', []) this.triggerEvent('requiringToSaveCurrentfile', [])
}, 5000) }, 5000)
} }
_switchSession (path) { _switchSession (path) {
this.currentSession = path this.triggerEvent('sessionSwitched', [])
this.editor.setSession(this.sessions[this.currentSession]) this.currentFile = path
this.editor.setReadOnly(this.readOnlySessions[this.currentSession]) this.renderComponent()
this.editor.focus()
} }
/** /**
@ -283,17 +174,27 @@ class Editor extends Plugin {
} }
/** /**
* Create an Ace session * Create an editor session
* @param {string} path path of the file
* @param {string} content Content of the file to open * @param {string} content Content of the file to open
* @param {string} mode Ace Mode for this file [Default is `text`] * @param {string} mode Mode for this file [Default is `text`]
*/ */
_createSession (content, mode) { _createSession (path, content, mode) {
const s = new ace.EditSession(content) if (!this.activated) return
s.setMode(mode || 'ace/mode/text') this.emit('addModel', content, mode, path, false)
s.setUndoManager(new ace.UndoManager()) return {
s.setTabSize(4) path,
s.setUseSoftTabs(true) language: mode,
return s setValue: (content) => {
this.emit('setValue', path, content)
},
getValue: () => {
return this.api.getValue(path, content)
},
dispose: () => {
this.emit('disposeModel', path)
}
}
} }
/** /**
@ -301,36 +202,16 @@ class Editor extends Plugin {
* @param {string} string * @param {string} string
*/ */
find (string) { find (string) {
return this.editor.find(string) return this.api.findMatches(this.currentFile, string)
} }
/** /**
* Display an Empty read-only session * Display an Empty read-only session
*/ */
displayEmptyReadOnlySession () { displayEmptyReadOnlySession () {
this.currentSession = null if (!this.activated) return
this.editor.setSession(this.emptySession) this.currentFile = null
this.editor.setReadOnly(true) this.emit('addModel', '', 'text', '_blank', true)
}
/**
* Sets a breakpoint on the row number
* @param {number} row Line index of the breakpoint
* @param {string} className Class of the breakpoint
*/
setBreakpoint (row, className) {
this.editor.session.setBreakpoint(row, className)
}
/**
* Increment the font size (in pixels) for the editor text.
* @param {number} incr The amount of pixels to add to the font.
*/
editorFontSize (incr) {
const newSize = this.editor.getFontSize() + incr
if (newSize >= 6) {
this.editor.setFontSize(newSize)
}
} }
/** /**
@ -338,8 +219,8 @@ class Editor extends Plugin {
* @param {string} text New text to be place. * @param {string} text New text to be place.
*/ */
setText (text) { setText (text) {
if (this.currentSession && this.sessions[this.currentSession]) { if (this.currentFile && this.sessions[this.currentFile]) {
this.sessions[this.currentSession].setValue(text) this.sessions[this.currentFile].setValue(text)
} }
} }
@ -356,7 +237,7 @@ class Editor extends Plugin {
- URL not prepended with the file explorer. We assume (as it is in the whole app, that this is a "browser" URL - URL not prepended with the file explorer. We assume (as it is in the whole app, that this is a "browser" URL
*/ */
if (!this.sessions[path]) { if (!this.sessions[path]) {
const session = this._createSession(content, this._getMode(path)) const session = this._createSession(path, content, this._getMode(path))
this.sessions[path] = session this.sessions[path] = session
this.readOnlySessions[path] = false this.readOnlySessions[path] = false
} else if (this.sessions[path].getValue() !== content) { } else if (this.sessions[path].getValue() !== content) {
@ -372,7 +253,7 @@ class Editor extends Plugin {
*/ */
openReadOnly (path, content) { openReadOnly (path, content) {
if (!this.sessions[path]) { if (!this.sessions[path]) {
const session = this._createSession(content, this._getMode(path)) const session = this._createSession(path, content, this._getMode(path))
this.sessions[path] = session this.sessions[path] = session
this.readOnlySessions[path] = true this.readOnlySessions[path] = true
} }
@ -394,8 +275,8 @@ class Editor extends Plugin {
* @return {String} content of the file referenced by @arg path * @return {String} content of the file referenced by @arg path
*/ */
get (path) { get (path) {
if (!path || this.currentSession === path) { if (!path || this.currentFile === path) {
return this.editor.getValue() return this.api.getValue(path)
} else if (this.sessions[path]) { } else if (this.sessions[path]) {
return this.sessions[path].getValue() return this.sessions[path].getValue()
} }
@ -407,29 +288,23 @@ class Editor extends Plugin {
* @return {String} path of the current session * @return {String} path of the current session
*/ */
current () { current () {
if (this.editor.getSession() === this.emptySession) { return this.currentFile
return
}
return this.currentSession
} }
/** /**
* The position of the cursor * The position of the cursor
*/ */
getCursorPosition () { getCursorPosition () {
return this.editor.session.doc.positionToIndex( return this.api.getCursorPosition()
this.editor.getCursorPosition(),
0
)
} }
/** /**
* Remove the current session from the list of sessions. * Remove the current session from the list of sessions.
*/ */
discardCurrentSession () { discardCurrentSession () {
if (this.sessions[this.currentSession]) { if (this.sessions[this.currentFile]) {
delete this.sessions[this.currentSession] delete this.sessions[this.currentFile]
this.currentSession = null this.currentFile = null
} }
} }
@ -438,73 +313,56 @@ class Editor extends Plugin {
* @param {string} path * @param {string} path
*/ */
discard (path) { discard (path) {
if (this.sessions[path]) delete this.sessions[path] if (this.sessions[path]) {
if (this.currentSession === path) this.currentSession = null this.sessions[path].dispose()
delete this.sessions[path]
}
if (this.currentFile === path) this.currentFile = null
} }
/** /**
* Resize the editor, and sets whether or not line wrapping is enabled. * Increment the font size (in pixels) for the editor text.
* @param {boolean} useWrapMode Enable (or disable) wrap mode * @param {number} incr The amount of pixels to add to the font.
*/ */
resize (useWrapMode) { editorFontSize (incr) {
this.editor.resize() if (!this.activated) return
const session = this.editor.getSession() const newSize = this.api.getFontSize() + incr
session.setUseWrapMode(useWrapMode) if (newSize >= 6) {
if (session.getUseWrapMode()) { this.emit('setFontSize', newSize)
const characterWidth = this.editor.renderer.characterWidth
const contentWidth = this.editor.container.ownerDocument.getElementsByClassName(
'ace_scroller'
)[0].clientWidth
if (contentWidth > 0) {
session.setWrapLimit(parseInt(contentWidth / characterWidth, 10))
}
} }
} }
/** /**
* Adds a new marker to the given `Range`. * Resize the editor, and sets whether or not line wrapping is enabled.
* @param {*} lineColumnPos * @param {boolean} useWrapMode Enable (or disable) wrap mode
* @param {string} source Path of the session to add the mark on.
* @param {string} cssClass css to apply to the mark.
*/ */
addMarker (lineColumnPos, source, cssClass) { resize (useWrapMode) {
const currentRange = new Range( if (!this.activated) return
lineColumnPos.start.line, this.emit('setWordWrap', useWrapMode)
lineColumnPos.start.column,
lineColumnPos.end.line,
lineColumnPos.end.column
)
if (this.sessions[source]) {
return this.sessions[source].addMarker(currentRange, cssClass)
}
return null
} }
/** /**
* Scrolls to a line. If center is true, it puts the line in middle of screen (or attempts to). * Moves the cursor and focus to the specified line and column number
* @param {number} line The line to scroll to * @param {number} line
* @param {boolean} center If true * @param {number} col
* @param {boolean} animate If true animates scrolling
* @param {Function} callback Function to be called when the animation has finished
*/ */
scrollToLine (line, center, animate, callback) { gotoLine (line, col) {
this.editor.scrollToLine(line, center, animate, callback) if (!this.activated) return
this.emit('focus')
this.emit('revealLine', line + 1, col)
} }
/** /**
* Remove a marker from the session * Scrolls to a line. If center is true, it puts the line in middle of screen (or attempts to).
* @param {string} markerId Id of the marker * @param {number} line The line to scroll to
* @param {string} source Path of the session
*/ */
removeMarker (markerId, source) { scrollToLine (line) {
if (this.sessions[source]) { if (!this.activated) return
this.sessions[source].removeMarker(markerId) this.emit('revealLine', line + 1, 0)
}
} }
/** /**
* Clears all the annotations for the given @arg filePath and @arg plugin, if none is given, the current sesssion is used. * Clears all the decorations for the given @arg filePath and @arg plugin, if none is given, the current sesssion is used.
* An annotation has the following shape: * An annotation has the following shape:
column: -1 column: -1
row: -1 row: -1
@ -512,64 +370,79 @@ class Editor extends Plugin {
type: "warning" type: "warning"
* @param {String} filePath * @param {String} filePath
* @param {String} plugin * @param {String} plugin
* @param {String} typeOfDecoration
*/ */
clearAnnotationsByPlugin (filePath, plugin) { clearDecorationsByPlugin (filePath, plugin, typeOfDecoration) {
if (filePath && !this.sessions[filePath]) throw new Error('file not found' + filePath) if (filePath && !this.sessions[filePath]) throw new Error('file not found' + filePath)
const session = this.sessions[filePath] || this.editor.getSession() const path = filePath || this.currentFile
const path = filePath || this.currentSession
const currentAnnotations = this.sourceAnnotationsPerFile[path] const currentAnnotations = this[typeOfDecoration][path]
if (!currentAnnotations) return if (!currentAnnotations) return
const newAnnotations = [] const newAnnotations = []
for (const annotation of currentAnnotations) { for (const annotation of currentAnnotations) {
if (annotation.from !== plugin) newAnnotations.push(annotation) if (annotation.from !== plugin) newAnnotations.push(annotation)
} }
this.sourceAnnotationsPerFile[path] = newAnnotations
this._setAnnotations(session, path) this[typeOfDecoration][path] = newAnnotations
this.renderComponent()
} }
keepAnnotationsFor (name) { keepDecorationsFor (name, typeOfDecoration) {
if (!this.currentSession) return if (!this.currentFile) return
if (!this.sourceAnnotationsPerFile[this.currentSession]) return if (!this[typeOfDecoration][this.currentFile]) return
const annotations = this.sourceAnnotationsPerFile[this.currentSession] const annotations = this[typeOfDecoration][this.currentFile]
for (const annotation of annotations) { for (const annotation of annotations) {
annotation.hide = annotation.from !== name annotation.hide = annotation.from !== name
} }
this.renderComponent()
this._setAnnotations(this.editor.getSession(), this.currentSession)
} }
/** /**
* Clears all the annotations for the given @arg filePath, the plugin name is retrieved from the context, if none is given, the current sesssion is used. * Clears all the decorations and for all the sessions for the given @arg plugin
* An annotation has the following shape: * An annotation has the following shape:
column: -1 column: -1
row: -1 row: -1
text: "browser/Untitled1.sol: Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.↵" text: "browser/Untitled1.sol: Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.↵"
type: "warning" type: "warning"
* @param {String} filePath * @param {String} filePath
* @param {String} plugin
*/ */
clearAnnotations (filePath) { clearAllDecorationsFor (plugin) {
const { from } = this.currentRequest for (const session in this.sessions) {
this.clearAnnotationsByPlugin(filePath, from) this.clearDecorationsByPlugin(session, plugin, 'sourceAnnotationsPerFile')
this.clearDecorationsByPlugin(session, plugin, 'markerPerFile')
}
} }
/** /**
* Clears all the annotations and for all the sessions for the given @arg plugin * Clears all the annotations for the given @arg filePath, the plugin name is retrieved from the context, if none is given, the current sesssion is used.
* An annotation has the following shape: * An annotation has the following shape:
column: -1 column: -1
row: -1 row: -1
text: "browser/Untitled1.sol: Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.↵" text: "browser/Untitled1.sol: Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.↵"
type: "warning" type: "warning"
* @param {String} filePath * @param {String} filePath
* @param {String} plugin
*/ */
clearAllAnnotationsFor (plugin) { clearAnnotations (filePath) {
for (const session in this.sessions) { filePath = filePath || this.currentFile
this.clearAnnotationsByPlugin(session, plugin) const { from } = this.currentRequest
} this.clearDecorationsByPlugin(filePath, from, 'sourceAnnotationsPerFile')
}
async addDecoration (decoration, filePath, typeOfDecoration) {
if (!filePath) return
filePath = await this.call('fileManager', 'getPathFromUrl', filePath)
filePath = filePath.file
if (!this.sessions[filePath]) throw new Error('file not found' + filePath)
const path = filePath || this.currentFile
const { from } = this.currentRequest
if (!this[typeOfDecoration][path]) this[typeOfDecoration][path] = []
decoration.from = from
this[typeOfDecoration][path].push(decoration)
this.renderComponent()
} }
/** /**
@ -582,32 +455,25 @@ class Editor extends Plugin {
* @param {Object} annotation * @param {Object} annotation
* @param {String} filePath * @param {String} filePath
*/ */
addAnnotation (annotation, filePath) { async addAnnotation (annotation, filePath) {
if (filePath && !this.sessions[filePath]) throw new Error('file not found' + filePath) filePath = filePath || this.currentFile
const session = this.sessions[filePath] || this.editor.getSession() await this.addDecoration(annotation, filePath, 'sourceAnnotationsPerFile')
const path = filePath || this.currentSession
const { from } = this.currentRequest
if (!this.sourceAnnotationsPerFile[path]) this.sourceAnnotationsPerFile[path] = []
annotation.from = from
this.sourceAnnotationsPerFile[path].push(annotation)
this._setAnnotations(session, path)
} }
_setAnnotations (session, path) { async highlight (position, filePath, highlightColor, opt = { focus: true }) {
const annotations = this.sourceAnnotationsPerFile[path] filePath = filePath || this.currentFile
session.setAnnotations(annotations.filter((element) => !element.hide)) if (opt.focus) {
await this.call('fileManager', 'open', filePath)
this.scrollToLine(position.start.line)
}
await this.addDecoration({ position }, filePath, 'markerPerFile')
} }
/** discardHighlight () {
* Moves the cursor and focus to the specified line and column number const { from } = this.currentRequest
* @param {number} line for (const session in this.sessions) {
* @param {number} col this.clearDecorationsByPlugin(session, from, 'markerPerFile')
*/ }
gotoLine (line, col) {
this.editor.focus()
this.editor.gotoLine(line + 1, col - 1, true)
} }
} }

@ -1,86 +0,0 @@
'use strict'
const csjs = require('csjs-inject')
const globlalRegistry = require('../../global/registry')
class SourceHighlighter {
constructor (localRegistry) {
this._components = {}
this._components.registry = localRegistry || globlalRegistry
// dependencies
this._deps = {
editor: this._components.registry.get('editor').api,
config: this._components.registry.get('config').api,
fileManager: this._components.registry.get('filemanager').api,
compilerArtefacts: this._components.registry.get('compilersartefacts').api
}
this.position = null
this.statementMarker = null
this.fullLineMarker = null
this.source = null
}
currentSourceLocation (lineColumnPos, location) {
if (this.statementMarker) this._deps.editor.removeMarker(this.statementMarker, this.source)
if (this.fullLineMarker) this._deps.editor.removeMarker(this.fullLineMarker, this.source)
const lastCompilationResult = this._deps.compilerArtefacts.__last
if (location && location.file !== undefined && lastCompilationResult) {
const path = lastCompilationResult.getSourceName(location.file)
if (path) {
this.currentSourceLocationFromfileName(lineColumnPos, path)
}
}
}
async currentSourceLocationFromfileName (lineColumnPos, filePath, style) {
if (this.statementMarker) this._deps.editor.removeMarker(this.statementMarker, this.source)
if (this.fullLineMarker) this._deps.editor.removeMarker(this.fullLineMarker, this.source)
this.statementMarker = null
this.fullLineMarker = null
this.source = null
if (lineColumnPos && lineColumnPos.start && lineColumnPos.end) {
this.source = filePath
this.style = style || 'var(--info)'
// if (!this.source) this.source = this._deps.fileManager.currentFile()
if (this._deps.fileManager.currentFile() !== this.source) {
await this._deps.fileManager.open(this.source)
this.source = this._deps.fileManager.currentFile()
}
const css = csjs`
.highlightcode {
position:absolute;
z-index:20;
opacity: 0.3;
background-color: ${this.style};
}
.highlightcode_fullLine {
position:absolute;
z-index:20;
opacity: 0.5;
background-color: ${this.style};
}
.customBackgroundColor {
background-color: ${this.style};
}
`
this.statementMarker = this._deps.editor.addMarker(lineColumnPos, this.source, css.highlightcode.className + ' ' + css.customBackgroundColor.className + ' ' + `highlightLine${lineColumnPos.start.line}`)
this._deps.editor.scrollToLine(lineColumnPos.start.line, true, true, function () {})
this.position = lineColumnPos
if (lineColumnPos.start.line === lineColumnPos.end.line) {
this.fullLineMarker = this._deps.editor.addMarker({
start: {
line: lineColumnPos.start.line,
column: 0
},
end: {
line: lineColumnPos.start.line + 1,
column: 0
}
}, this.source, css.highlightcode_fullLine.className)
}
}
}
}
module.exports = SourceHighlighter

@ -227,7 +227,7 @@ class DGitProvider extends Plugin {
const permission = await this.askUserPermission('clone', 'Import multiple files into your workspaces.') const permission = await this.askUserPermission('clone', 'Import multiple files into your workspaces.')
if (!permission) return false if (!permission) return false
if (this.calculateLocalStorage() > 10000) throw new Error('The local storage of the browser is full.') if (this.calculateLocalStorage() > 10000) throw new Error('The local storage of the browser is full.')
await this.call('filePanel', 'createWorkspace', `workspace_${Date.now()}`, false) await this.call('filePanel', 'createWorkspace', `workspace_${Date.now()}`, true)
const cmd = { const cmd = {
url: input.url, url: input.url,
@ -459,7 +459,7 @@ class DGitProvider extends Plugin {
if (!permission) return false if (!permission) return false
if (this.calculateLocalStorage() > 10000) throw new Error('The local storage of the browser is full.') if (this.calculateLocalStorage() > 10000) throw new Error('The local storage of the browser is full.')
const cid = cmd.cid const cid = cmd.cid
await this.call('filePanel', 'createWorkspace', `workspace_${Date.now()}`, false) await this.call('filePanel', 'createWorkspace', `workspace_${Date.now()}`, true)
const workspace = await this.call('filePanel', 'getCurrentWorkspace') const workspace = await this.call('filePanel', 'getCurrentWorkspace')
let result let result
if (cmd.local) { if (cmd.local) {

@ -140,7 +140,7 @@ class FileManager extends Plugin {
refresh () { refresh () {
const provider = this.fileProviderOf('/') const provider = this.fileProviderOf('/')
// emit rootFolderChanged so that File Explorer reloads the file tree // emit rootFolderChanged so that File Explorer reloads the file tree
provider.event.emit('rootFolderChanged') provider.event.emit('rootFolderChanged', provider.workspace || '/')
} }
/** /**
@ -456,7 +456,7 @@ class FileManager extends Plugin {
return this._deps.config.get('currentFile') return this._deps.config.get('currentFile')
} }
closeAllFiles () { async closeAllFiles () {
// TODO: Only keep `this.emit` (issue#2210) // TODO: Only keep `this.emit` (issue#2210)
this.emit('filesAllClosed') this.emit('filesAllClosed')
this.events.emit('filesAllClosed') this.events.emit('filesAllClosed')
@ -465,7 +465,7 @@ class FileManager extends Plugin {
} }
} }
closeFile (name) { async closeFile (name) {
delete this.openedFiles[name] delete this.openedFiles[name]
if (!Object.keys(this.openedFiles).length) { if (!Object.keys(this.openedFiles).length) {
this._deps.config.set('currentFile', '') this._deps.config.set('currentFile', '')
@ -506,7 +506,8 @@ class FileManager extends Plugin {
async setFileContent (path, content) { async setFileContent (path, content) {
if (this.currentRequest) { if (this.currentRequest) {
const canCall = await this.askUserPermission('writeFile', '') const canCall = await this.askUserPermission('writeFile', '')
if (canCall) { 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 // inform the user about modification after permission is granted and even if permission was saved before
toaster(yo` toaster(yo`
<div> <div>
@ -615,6 +616,7 @@ class FileManager extends Plugin {
this.emit('noFileSelected') this.emit('noFileSelected')
this.events.emit('noFileSelected') this.events.emit('noFileSelected')
} else { } else {
file = this.normalize(file)
this.saveCurrentFile() this.saveCurrentFile()
const resolved = this.getPathFromUrl(file) const resolved = this.getPathFromUrl(file)
file = resolved.file file = resolved.file

@ -15,9 +15,9 @@ module.exports = class RemixDProvider extends FileProvider {
_registerEvent () { _registerEvent () {
var remixdEvents = ['connecting', 'connected', 'errored', 'closed'] var remixdEvents = ['connecting', 'connected', 'errored', 'closed']
remixdEvents.forEach((value) => { remixdEvents.forEach((event) => {
this._appManager.on('remixd', value, (event) => { this._appManager.on('remixd', event, (value) => {
this.event.emit(value, event) this.event.emit(event, value)
}) })
}) })
@ -41,8 +41,18 @@ module.exports = class RemixDProvider extends FileProvider {
this.event.emit('fileRenamed', oldPath, newPath) this.event.emit('fileRenamed', oldPath, newPath)
}) })
this._appManager.on('remixd', 'rootFolderChanged', () => { this._appManager.on('remixd', 'rootFolderChanged', (path) => {
this.event.emit('rootFolderChanged') this.event.emit('rootFolderChanged', path)
})
this._appManager.on('remixd', 'removed', (path) => {
this.event.emit('fileRemoved', path)
})
this._appManager.on('remixd', 'changed', (path) => {
this.get(path, (_error, content) => {
this.event.emit('fileExternallyChanged', path, content)
})
}) })
} }
@ -57,7 +67,7 @@ module.exports = class RemixDProvider extends FileProvider {
} }
preInit () { preInit () {
this.event.emit('loading') this.event.emit('loadingLocalhost')
} }
init (cb) { init (cb) {
@ -66,6 +76,7 @@ module.exports = class RemixDProvider extends FileProvider {
.then((result) => { .then((result) => {
this._isReady = true this._isReady = true
this._readOnlyMode = result this._readOnlyMode = result
this.event.emit('readOnlyModeChanged', result)
this._registerEvent() this._registerEvent()
this.event.emit('connected') this.event.emit('connected')
cb && cb() cb && cb()
@ -177,9 +188,7 @@ module.exports = class RemixDProvider extends FileProvider {
} }
resolveDirectory (path, callback) { resolveDirectory (path, callback) {
var self = this
if (path[0] === '/') path = path.substring(1) if (path[0] === '/') path = path.substring(1)
if (!path) return callback(null, { [self.type]: { } })
const unprefixedpath = this.removePrefix(path) const unprefixedpath = this.removePrefix(path)
if (!this._isReady) return callback && callback('provider not ready') if (!this._isReady) return callback && callback('provider not ready')

@ -13,6 +13,7 @@ class WorkspaceFileProvider extends FileProvider {
} }
setWorkspace (workspace) { setWorkspace (workspace) {
if (!workspace) return
workspace = workspace.replace(/^\/|\/$/g, '') // remove first and last slash workspace = workspace.replace(/^\/|\/$/g, '') // remove first and last slash
this.workspace = workspace this.workspace = workspace
} }
@ -30,10 +31,15 @@ class WorkspaceFileProvider extends FileProvider {
} }
removePrefix (path) { removePrefix (path) {
if (!this.workspace) this.createWorkspace()
path = path.replace(/^\/|\/$/g, '') // remove first and last slash path = path.replace(/^\/|\/$/g, '') // remove first and last slash
if (path.startsWith(this.workspacesPath + '/' + this.workspace)) return path if (path.startsWith(this.workspacesPath + '/' + this.workspace)) return path
if (path.startsWith(this.workspace)) return path.replace(this.workspace, this.workspacesPath + '/' + this.workspace) const splitPath = path.split('/')
if (splitPath[0] === this.workspace) {
splitPath[0] = this.workspacesPath + '/' + this.workspace
path = splitPath.join('/')
return path
}
path = super.removePrefix(path) path = super.removePrefix(path)
let ret = this.workspacesPath + '/' + this.workspace + '/' + (path === '/' ? '' : path) let ret = this.workspacesPath + '/' + this.workspace + '/' + (path === '/' ? '' : path)
@ -51,7 +57,6 @@ class WorkspaceFileProvider extends FileProvider {
} }
resolveDirectory (path, callback) { resolveDirectory (path, callback) {
if (!this.workspace) this.createWorkspace()
super.resolveDirectory(path, (error, files) => { super.resolveDirectory(path, (error, files) => {
if (error) return callback(error) if (error) return callback(error)
const unscoped = {} const unscoped = {}
@ -76,13 +81,18 @@ class WorkspaceFileProvider extends FileProvider {
} }
_normalizePath (path) { _normalizePath (path) {
if (!this.workspace) this.createWorkspace()
return path.replace(this.workspacesPath + '/' + this.workspace + '/', '') return path.replace(this.workspacesPath + '/' + this.workspace + '/', '')
} }
createWorkspace (name) { async createWorkspace (name) {
if (!name) name = 'default_workspace' try {
this.event.emit('createWorkspace', name) if (!name) name = 'default_workspace'
this.setWorkspace(name)
await super.createDir(name)
this.event.emit('createWorkspace', name)
} catch (e) {
throw new Error(e)
}
} }
} }

@ -3,18 +3,12 @@ import { ViewPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
import React from 'react' // eslint-disable-line import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { Workspace } from '@remix-ui/workspace' // eslint-disable-line import { FileSystemProvider } from '@remix-ui/workspace' // eslint-disable-line
import { bufferToHex, keccakFromString } from 'ethereumjs-util'
import { checkSpecialChars, checkSlash } from '../../lib/helper'
const { RemixdHandle } = require('../files/remixd-handle.js') const { RemixdHandle } = require('../files/remixd-handle.js')
const { GitHandle } = require('../files/git-handle.js') const { GitHandle } = require('../files/git-handle.js')
const { HardhatHandle } = require('../files/hardhat-handle.js') const { HardhatHandle } = require('../files/hardhat-handle.js')
const { SlitherHandle } = require('../files/slither-handle.js') const { SlitherHandle } = require('../files/slither-handle.js')
const globalRegistry = require('../../global/registry') const globalRegistry = require('../../global/registry')
const examples = require('../editor/examples')
const GistHandler = require('../../lib/gist-handler')
const QueryParams = require('../../lib/query-params')
const modalDialogCustom = require('../ui/modal-dialog-custom')
/* /*
Overview of APIs: Overview of APIs:
* fileManager: @args fileProviders (browser, shared-folder, swarm, github, etc ...) & config & editor * fileManager: @args fileProviders (browser, shared-folder, swarm, github, etc ...) & config & editor
@ -35,8 +29,8 @@ const modalDialogCustom = require('../ui/modal-dialog-custom')
const profile = { const profile = {
name: 'filePanel', name: 'filePanel',
displayName: 'File explorers', displayName: 'File explorers',
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem'], methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem', 'renameWorkspace', 'deleteWorkspace'],
events: ['setWorkspace', 'renameWorkspace', 'deleteWorkspace', 'createWorkspace'], events: ['setWorkspace', 'workspaceRenamed', 'workspaceDeleted', 'workspaceCreated'],
icon: 'assets/img/fileManager.webp', icon: 'assets/img/fileManager.webp',
description: ' - ', description: ' - ',
kind: 'fileexplorer', kind: 'fileexplorer',
@ -47,54 +41,33 @@ const profile = {
module.exports = class Filepanel extends ViewPlugin { module.exports = class Filepanel extends ViewPlugin {
constructor (appManager) { constructor (appManager) {
super(profile) super(profile)
this._components = {} this.registry = globalRegistry
this._components.registry = globalRegistry this.fileProviders = this.registry.get('fileproviders').api
this._deps = { this.fileManager = this.registry.get('filemanager').api
fileProviders: this._components.registry.get('fileproviders').api,
fileManager: this._components.registry.get('filemanager').api
}
this.el = document.createElement('div') this.el = document.createElement('div')
this.el.setAttribute('id', 'fileExplorerView') this.el.setAttribute('id', 'fileExplorerView')
this.remixdHandle = new RemixdHandle(this._deps.fileProviders.localhost, appManager) this.remixdHandle = new RemixdHandle(this.fileProviders.localhost, appManager)
this.gitHandle = new GitHandle() this.gitHandle = new GitHandle()
this.hardhatHandle = new HardhatHandle() this.hardhatHandle = new HardhatHandle()
this.slitherHandle = new SlitherHandle() this.slitherHandle = new SlitherHandle()
this.registeredMenuItems = []
this.removedMenuItems = []
this.request = {}
this.workspaces = [] this.workspaces = []
this.initialWorkspace = null
this.appManager = appManager this.appManager = appManager
this.currentWorkspaceMetadata = {}
}
onActivation () {
this.renderComponent()
} }
render () { render () {
this.initWorkspace().then(() => this.getWorkspaces()).catch(console.error)
return this.el return this.el
} }
renderComponent () { renderComponent () {
ReactDOM.render( ReactDOM.render(
<Workspace <FileSystemProvider plugin={this} />
createWorkspace={this.createWorkspace.bind(this)}
renameWorkspace={this.renameWorkspace.bind(this)}
setWorkspace={this.setWorkspace.bind(this)}
workspaceRenamed={this.workspaceRenamed.bind(this)}
workspaceDeleted={this.workspaceDeleted.bind(this)}
workspaceCreated={this.workspaceCreated.bind(this)}
workspace={this._deps.fileProviders.workspace}
browser={this._deps.fileProviders.browser}
localhost={this._deps.fileProviders.localhost}
fileManager={this._deps.fileManager}
registry={this._components.registry}
plugin={this}
request={this.request}
workspaces={this.workspaces}
registeredMenuItems={this.registeredMenuItems}
removedMenuItems={this.removedMenuItems}
initialWorkspace={this.initialWorkspace}
/>
, this.el) , this.el)
} }
@ -103,202 +76,103 @@ module.exports = class Filepanel extends ViewPlugin {
* @param callback (...args) => void * @param callback (...args) => void
*/ */
registerContextMenuItem (item) { registerContextMenuItem (item) {
if (!item) throw new Error('Invalid register context menu argument') return new Promise((resolve, reject) => {
if (!item.name || !item.id) throw new Error('Item name and id is mandatory') this.emit('registerContextMenuItemReducerEvent', item, (err, data) => {
if (!item.type && !item.path && !item.extension && !item.pattern) throw new Error('Invalid file matching criteria provided') if (err) reject(err)
if (this.registeredMenuItems.filter((o) => { else resolve(data)
return o.id === item.id && o.name === item.name })
}).length) throw new Error(`Action ${item.name} already exists on ${item.id}`) })
this.registeredMenuItems = [...this.registeredMenuItems, item]
this.removedMenuItems = this.removedMenuItems.filter(menuItem => item.id !== menuItem.id)
this.renderComponent()
} }
removePluginActions (plugin) { removePluginActions (plugin) {
this.registeredMenuItems = this.registeredMenuItems.filter((item) => { return new Promise((resolve, reject) => {
if (item.id !== plugin.name || item.sticky === true) return true this.emit('removePluginActionsReducerEvent', plugin, (err, data) => {
else { if (err) reject(err)
this.removedMenuItems.push(item) else resolve(data)
return false })
}
}) })
this.renderComponent()
} }
async getCurrentWorkspace () { getCurrentWorkspace () {
return await this.request.getCurrentWorkspace() return this.currentWorkspaceMetadata
} }
async getWorkspaces () { getWorkspaces () {
const result = new Promise((resolve, reject) => {
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
this._deps.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, items) => {
if (error) {
console.error(error)
return reject(error)
}
resolve(Object.keys(items)
.filter((item) => items[item].isDirectory)
.map((folder) => folder.replace(workspacesPath + '/', '')))
})
})
try {
this.workspaces = await result
} catch (e) {
modalDialogCustom.alert('Workspaces have not been created on your system. Please use "Migrate old filesystem to workspace" on the home page to transfer your files or start by creating a new workspace in the File Explorers.')
console.log(e)
}
this.renderComponent()
return this.workspaces return this.workspaces
} }
async initWorkspace () { setWorkspaces (workspaces) {
this.renderComponent() this.workspaces = workspaces
const queryParams = new QueryParams() }
const gistHandler = new GistHandler()
const params = queryParams.get()
// get the file from gist
let loadedFromGist = false
if (params.gist) {
await this.processCreateWorkspace('gist-sample')
this._deps.fileProviders.workspace.setWorkspace('gist-sample')
this.initialWorkspace = 'gist-sample'
loadedFromGist = gistHandler.loadFromGist(params, this._deps.fileManager)
}
if (loadedFromGist) return
if (params.code || params.url) {
try {
await this.processCreateWorkspace('code-sample')
this._deps.fileProviders.workspace.setWorkspace('code-sample')
let path = ''
let content = ''
if (params.code) {
var hash = bufferToHex(keccakFromString(params.code))
path = 'contract-' + hash.replace('0x', '').substring(0, 10) + '.sol'
content = atob(params.code)
await this._deps.fileProviders.workspace.set(path, content)
}
if (params.url) {
const data = await this.call('contentImport', 'resolve', params.url)
path = data.cleanUrl
content = data.content
await this._deps.fileProviders.workspace.set(path, content)
}
this.initialWorkspace = 'code-sample'
await this._deps.fileManager.openFile(path)
} catch (e) {
console.error(e)
}
return
}
const self = this createNewFile () {
this.appManager.on('manager', 'pluginDeactivated', self.removePluginActions.bind(this))
// insert example contracts if there are no files to show
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this._deps.fileProviders.browser.resolveDirectory('/', async (error, filesList) => { const provider = this.fileManager.currentFileProvider()
if (error) return reject(error) const dir = provider.workspace || '/'
if (Object.keys(filesList).length === 0) {
await this.createWorkspace('default_workspace')
resolve('default_workspace')
} else {
this._deps.fileProviders.browser.resolveDirectory('.workspaces', async (error, filesList) => {
if (error) return reject(error)
if (Object.keys(filesList).length > 0) {
const workspacePath = Object.keys(filesList)[0].split('/').filter(val => val)
const workspaceName = workspacePath[workspacePath.length - 1]
this._deps.fileProviders.workspace.setWorkspace(workspaceName) this.emit('createNewFileInputReducerEvent', dir, (err, data) => {
return resolve(workspaceName) if (err) reject(err)
} else resolve(data)
return reject(new Error('Can\'t find available workspace.'))
})
}
}) })
}) })
} }
async createNewFile () { uploadFile (target) {
return await this.request.createNewFile() return new Promise((resolve, reject) => {
} const provider = this.fileManager.currentFileProvider()
const dir = provider.workspace || '/'
async uploadFile (event) { return this.emit('uploadFileReducerEvent', dir, target, (err, data) => {
return await this.request.uploadFile(event) if (err) reject(err)
else resolve(data)
})
})
} }
async processCreateWorkspace (name) { createWorkspace (workspaceName, isEmpty) {
const workspaceProvider = this._deps.fileProviders.workspace return new Promise((resolve, reject) => {
const browserProvider = this._deps.fileProviders.browser this.emit('createWorkspaceReducerEvent', workspaceName, isEmpty, (err, data) => {
const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name if (err) reject(err)
const workspaceRootPath = 'browser/' + workspaceProvider.workspacesPath else resolve(data || true)
const workspaceRootPathExists = await browserProvider.exists(workspaceRootPath) })
const workspacePathExists = await browserProvider.exists(workspacePath) })
if (!workspaceRootPathExists) browserProvider.createDir(workspaceRootPath)
if (!workspacePathExists) browserProvider.createDir(workspacePath)
} }
async workspaceExists (name) { renameWorkspace (oldName, workspaceName) {
const workspaceProvider = this._deps.fileProviders.workspace return new Promise((resolve, reject) => {
const browserProvider = this._deps.fileProviders.browser this.emit('renameWorkspaceReducerEvent', oldName, workspaceName, (err, data) => {
const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name if (err) reject(err)
return browserProvider.exists(workspacePath) else resolve(data || true)
})
})
} }
async createWorkspace (workspaceName, setDefaults = true) { deleteWorkspace (workspaceName) {
if (!workspaceName) throw new Error('name cannot be empty') return new Promise((resolve, reject) => {
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed') this.emit('deleteWorkspaceReducerEvent', workspaceName, (err, data) => {
if (await this.workspaceExists(workspaceName)) throw new Error('workspace already exists') if (err) reject(err)
else { else resolve(data || true)
const workspaceProvider = this._deps.fileProviders.workspace })
await this.processCreateWorkspace(workspaceName) })
workspaceProvider.setWorkspace(workspaceName)
await this.request.setWorkspace(workspaceName) // tells the react component to switch to that workspace
if (setDefaults) {
for (const file in examples) {
try {
await workspaceProvider.set(examples[file].name, examples[file].content)
} catch (error) {
console.error(error)
}
}
}
}
} }
async renameWorkspace (oldName, workspaceName) { setWorkspace (workspace) {
if (!workspaceName) throw new Error('name cannot be empty') const workspaceProvider = this.fileProviders.workspace
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
if (await this.workspaceExists(workspaceName)) throw new Error('workspace already exists')
const browserProvider = this._deps.fileProviders.browser
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
browserProvider.rename('browser/' + workspacesPath + '/' + oldName, 'browser/' + workspacesPath + '/' + workspaceName, true)
}
/** these are called by the react component, action is already finished whent it's called */ this.currentWorkspaceMetadata = { name: workspace.name, isLocalhost: workspace.isLocalhost, absolutePath: `${workspaceProvider.workspacesPath}/${workspace.name}` }
async setWorkspace (workspace, setEvent = true) { this.emit('setWorkspace', workspace)
if (workspace.isLocalhost) {
this.call('manager', 'activatePlugin', 'remixd')
} else if (await this.call('manager', 'isActive', 'remixd')) {
this.call('manager', 'deactivatePlugin', 'remixd')
}
if (setEvent) {
this._deps.fileManager.setMode(workspace.isLocalhost ? 'localhost' : 'browser')
this.emit('setWorkspace', workspace)
}
} }
workspaceRenamed (workspace) { workspaceRenamed (oldName, workspaceName) {
this.emit('renameWorkspace', workspace) this.emit('workspaceRenamed', oldName, workspaceName)
} }
workspaceDeleted (workspace) { workspaceDeleted (workspace) {
this.emit('deleteWorkspace', workspace) this.emit('workspaceDeleted', workspace)
} }
workspaceCreated (workspace) { workspaceCreated (workspace) {
this.emit('createWorkspace', workspace) this.emit('workspaceCreated', workspace)
} }
/** end section */ /** end section */
} }

@ -54,7 +54,7 @@ export class MainView {
fileManager: self._components.registry.get('filemanager').api fileManager: self._components.registry.get('filemanager').api
} }
self.tabProxy = new TabProxy(self.fileManager, self.editor, self.appManager) self.tabProxy = new TabProxy(self.fileManager, self.editor)
/* /*
We listen here on event from the tab component to display / hide the editor and mainpanel We listen here on event from the tab component to display / hide the editor and mainpanel
depending on the content that should be displayed depending on the content that should be displayed

@ -1,17 +1,9 @@
import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
import { Plugin } from '@remixproject/engine' import { Plugin } from '@remixproject/engine'
const yo = require('yo-yo') import { TabsUI } from '@remix-ui/tabs'
const $ = require('jquery')
const EventEmitter = require('events') const EventEmitter = require('events')
const globalRegistry = require('../../global/registry')
const csjs = require('csjs-inject')
const helper = require('../../lib/helper') const helper = require('../../lib/helper')
require('remix-tabs')
const css = csjs`
.remix_tabs div[title]{
display: flex;
}
`
const profile = { const profile = {
name: 'tabs', name: 'tabs',
@ -21,46 +13,60 @@ const profile = {
// @todo(#650) Merge this with MainPanel into one plugin // @todo(#650) Merge this with MainPanel into one plugin
export class TabProxy extends Plugin { export class TabProxy extends Plugin {
constructor (fileManager, editor, appManager) { constructor (fileManager, editor) {
super(profile) super(profile)
this.event = new EventEmitter() this.event = new EventEmitter()
this.fileManager = fileManager this.fileManager = fileManager
this.appManager = appManager
this.editor = editor this.editor = editor
this.data = {} this.data = {}
this._view = {} this._view = {}
this._handlers = {} this._handlers = {}
this.loadedTabs = [] this.loadedTabs = []
}
globalRegistry.get('themeModule').api.events.on('themeChanged', (theme) => { onActivation () {
// update invert for all icons this.on('theme', 'themeChanged', (theme) => {
// update invert for all icons
this.updateImgStyles() this.updateImgStyles()
}) })
fileManager.events.on('filesAllClosed', () => { this.on('fileManager', 'filesAllClosed', () => {
this.call('manager', 'activatePlugin', 'home') this.call('manager', 'activatePlugin', 'home')
this._view.filetabs.active = 'home' this.tabsApi.activateTab('home')
}) })
fileManager.events.on('fileRemoved', (name) => { this.on('fileManager', 'fileRemoved', (name) => {
const workspace = this.fileManager.currentWorkspace() const workspace = this.fileManager.currentWorkspace()
workspace ? this.removeTab(workspace + '/' + name) : this.removeTab(this.fileManager.mode + '/' + name)
if (this.fileManager.mode === 'browser') {
name = name.startsWith(workspace + '/') ? name : workspace + '/' + name
this.removeTab(name)
} else {
name = name.startsWith(this.fileManager.mode + '/') ? name : this.fileManager.mode + '/' + name
this.removeTab(name)
}
}) })
fileManager.events.on('fileClosed', (name) => { this.on('fileManager', 'fileClosed', (name) => {
const workspace = this.fileManager.currentWorkspace() const workspace = this.fileManager.currentWorkspace()
workspace ? this.removeTab(workspace + '/' + name) : this.removeTab(this.fileManager.mode + '/' + name) if (this.fileManager.mode === 'browser') {
name = name.startsWith(workspace + '/') ? name : workspace + '/' + name
this.removeTab(name)
} else {
name = name.startsWith(this.fileManager.mode + '/') ? name : this.fileManager.mode + '/' + name
this.removeTab(name)
}
}) })
fileManager.events.on('currentFileChanged', (file) => { this.on('fileManager', 'currentFileChanged', (file) => {
const workspace = this.fileManager.currentWorkspace() const workspace = this.fileManager.currentWorkspace()
if (workspace) { if (this.fileManager.mode === 'browser') {
const workspacePath = workspace + '/' + file const workspacePath = workspace + '/' + file
if (this._handlers[workspacePath]) { if (this._handlers[workspacePath]) {
this._view.filetabs.activateTab(workspacePath) this.tabsApi.activateTab(workspacePath)
return return
} }
this.addTab(workspacePath, '', () => { this.addTab(workspacePath, '', () => {
@ -71,11 +77,12 @@ export class TabProxy extends Plugin {
this.fileManager.closeFile(file) this.fileManager.closeFile(file)
this.event.emit('closeFile', file) this.event.emit('closeFile', file)
}) })
this.tabsApi.activateTab(workspacePath)
} else { } else {
const path = this.fileManager.mode + '/' + file const path = file.startsWith(this.fileManager.mode + '/') ? file : this.fileManager.mode + '/' + file
if (this._handlers[path]) { if (this._handlers[path]) {
this._view.filetabs.activateTab(path) this.tabsApi.activateTab(path)
return return
} }
this.addTab(path, '', () => { this.addTab(path, '', () => {
@ -86,13 +93,14 @@ export class TabProxy extends Plugin {
this.fileManager.closeFile(file) this.fileManager.closeFile(file)
this.event.emit('closeFile', file) this.event.emit('closeFile', file)
}) })
this.tabsApi.activateTab(path)
} }
}) })
fileManager.events.on('fileRenamed', (oldName, newName, isFolder) => { this.on('fileManager', 'fileRenamed', (oldName, newName, isFolder) => {
const workspace = this.fileManager.currentWorkspace() const workspace = this.fileManager.currentWorkspace()
if (workspace) { if (this.fileManager.mode === 'browser') {
if (isFolder) { if (isFolder) {
for (const tab of this.loadedTabs) { for (const tab of this.loadedTabs) {
if (tab.name.indexOf(workspace + '/' + oldName + '/') === 0) { if (tab.name.indexOf(workspace + '/' + oldName + '/') === 0) {
@ -115,11 +123,11 @@ export class TabProxy extends Plugin {
return return
} }
// should change the tab title too // should change the tab title too
this.renameTab(this.fileManager.mode + '/' + oldName, workspace + '/' + newName) this.renameTab(this.fileManager.mode + '/' + oldName, this.fileManager.mode + '/' + newName)
} }
}) })
appManager.event.on('activate', ({ name, location, displayName, icon }) => { this.on('manager', 'pluginActivated', ({ name, location, displayName, icon }) => {
if (location === 'mainPanel') { if (location === 'mainPanel') {
this.addTab( this.addTab(
name, name,
@ -135,32 +143,32 @@ export class TabProxy extends Plugin {
} }
}) })
appManager.event.on('deactivate', (profile) => { this.on('manager', 'pluginDeactivated', (profile) => {
this.removeTab(profile.name) this.removeTab(profile.name)
}) })
} }
focus (name) { focus (name) {
this.event.emit('switchApp', name) this.event.emit('switchApp', name)
this._view.filetabs.activateTab(name) this.tabsApi.activateTab(name)
} }
updateImgStyles () { updateImgStyles () {
const images = this._view.filetabs.getElementsByClassName('iconImage') const images = this.el.getElementsByClassName('iconImage')
for (const element of images) { for (const element of images) {
globalRegistry.get('themeModule').api.fixInvert(element) this.call('theme', 'fixInvert', element)
}; }
} }
switchTab (tabName) { switchTab (tabName) {
if (this._handlers[tabName]) { if (this._handlers[tabName]) {
this._handlers[tabName].switchTo() this._handlers[tabName].switchTo()
this._view.filetabs.activateTab(tabName) this.tabsApi.activateTab(tabName)
} }
} }
switchNextTab () { switchNextTab () {
const active = this._view.filetabs.active const active = this.tabsApi.active()
if (active && this._handlers[active]) { if (active && this._handlers[active]) {
const handlers = Object.keys(this._handlers) const handlers = Object.keys(this._handlers)
let i = handlers.indexOf(active) let i = handlers.indexOf(active)
@ -172,7 +180,7 @@ export class TabProxy extends Plugin {
} }
switchPreviousTab () { switchPreviousTab () {
const active = this._view.filetabs.active const active = this.tabsApi.active()
if (active && this._handlers[active]) { if (active && this._handlers[active]) {
const handlers = Object.keys(this._handlers) const handlers = Object.keys(this._handlers)
let i = handlers.indexOf(active) let i = handlers.indexOf(active)
@ -184,7 +192,7 @@ export class TabProxy extends Plugin {
} }
switchToActiveTab () { switchToActiveTab () {
const active = this._view.filetabs.active const active = this.tabsApi.active()
if (active && this._handlers[active]) { if (active && this._handlers[active]) {
this.switchTab(active) this.switchTab(active)
} }
@ -203,7 +211,7 @@ export class TabProxy extends Plugin {
} }
addTab (name, title, switchTo, close, icon) { addTab (name, title, switchTo, close, icon) {
if (this._handlers[name]) return if (this._handlers[name]) return this.renderComponent()
var slash = name.split('/') var slash = name.split('/')
const tabPath = slash.reverse() const tabPath = slash.reverse()
@ -219,8 +227,12 @@ export class TabProxy extends Plugin {
title = formatPath.join('/') title = formatPath.join('/')
const titleLength = formatPath.length const titleLength = formatPath.length
this.loadedTabs.push({ this.loadedTabs.push({
id: name,
name, name,
title title,
icon,
tooltip: name,
iconClass: helper.getPathIcon(name)
}) })
formatPath.shift() formatPath.shift()
if (formatPath.length > 0) { if (formatPath.length > 0) {
@ -230,12 +242,8 @@ export class TabProxy extends Plugin {
const duplicateTabTitle = duplicateTabFormatPath.slice(0, titleLength).reverse().join('/') const duplicateTabTitle = duplicateTabFormatPath.slice(0, titleLength).reverse().join('/')
this.loadedTabs.push({ this.loadedTabs.push({
name: duplicateTabName,
title: duplicateTabTitle
})
this._view.filetabs.removeTab(duplicateTabName)
this._view.filetabs.addTab({
id: duplicateTabName, id: duplicateTabName,
name: duplicateTabName,
title: duplicateTabTitle, title: duplicateTabTitle,
icon, icon,
tooltip: duplicateTabName, tooltip: duplicateTabName,
@ -247,27 +255,25 @@ export class TabProxy extends Plugin {
} }
} else { } else {
this.loadedTabs.push({ this.loadedTabs.push({
id: name,
name, name,
title title,
icon,
tooltip: name,
iconClass: helper.getPathIcon(name)
}) })
} }
this._view.filetabs.addTab({ this.renderComponent()
id: name,
title,
icon,
tooltip: name,
iconClass: helper.getPathIcon(name)
})
this.updateImgStyles() this.updateImgStyles()
this._handlers[name] = { switchTo, close } this._handlers[name] = { switchTo, close }
} }
removeTab (name) { removeTab (name) {
this._view.filetabs.removeTab(name)
delete this._handlers[name] delete this._handlers[name]
this.switchToActiveTab() this.switchToActiveTab()
this.loadedTabs = this.loadedTabs.filter(tab => tab.name !== name) this.loadedTabs = this.loadedTabs.filter(tab => tab.name !== name)
this.renderComponent()
this.updateImgStyles() this.updateImgStyles()
} }
@ -275,62 +281,36 @@ export class TabProxy extends Plugin {
this.handlers[type] = fn this.handlers[type] = fn
} }
onZoomOut () { renderComponent () {
this.editor.editorFontSize(-1) const onSelect = (index) => {
} 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)
}
}
onZoomIn () { const onClose = (index) => {
this.editor.editorFontSize(1) 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)
}
}
renderTabsbar () { const onZoomIn = () => this.editor.editorFontSize(1)
this._view.filetabs = yo`<remix-tabs class=${css.remix_tabs}></remix-tabs>` const onZoomOut = () => this.editor.editorFontSize(-1)
this._view.filetabs.addEventListener('tabClosed', (event) => {
if (this._handlers[event.detail]) this._handlers[event.detail].close()
this.event.emit('tabCountChanged', this._view.filetabs.tabs.length)
})
this._view.filetabs.addEventListener('tabActivated', (event) => {
if (this._handlers[event.detail]) this._handlers[event.detail].switchTo()
this.event.emit('tabCountChanged', this._view.filetabs.tabs.length)
})
this._view.filetabs.canAdd = false const onReady = (api) => { this.tabsApi = api }
const zoomBtns = yo`
<div class="d-flex flex-row justify-content-center align-items-center">
<span data-id="tabProxyZoomOut" class="btn btn-sm px-1 fas fa-search-minus text-dark" onclick=${() => this.onZoomOut()}></span>
<span data-id="tabProxyZoomIn" class="btn btn-sm px-1 fas fa-search-plus text-dark" onclick=${() => this.onZoomIn()}></span>
</div>
`
// @todo(#2492) remove style after the mainPanel layout fix.
this._view.tabs = yo`
<div class="mainview--tabs-proxy" style="display: -webkit-box; max-height: 32px">
${zoomBtns}
${this._view.filetabs}
</div>
`
// tabs
var $filesEl = $(this._view.filetabs)
// Switch tab
var self = this
$filesEl.on('click', '.file:not(.active)', function (ev) {
ev.preventDefault()
var name = $(this).find('.name').text()
self._handlers[name].switchTo()
return false
})
// Remove current tab ReactDOM.render(
$filesEl.on('click', '.file .remove', function (ev) { <TabsUI tabs={this.loadedTabs} onSelect={onSelect} onClose={onClose} onZoomIn={onZoomIn} onZoomOut={onZoomOut} onReady={onReady} />
ev.preventDefault() , this.el)
var name = $(this).parent().find('.name').text() }
self._handlers[name].close()
return false
})
return this._view.tabs renderTabsbar () {
this.el = document.createElement('div')
this.renderComponent()
return this.el
} }
} }

@ -1,29 +1,23 @@
/* global Node, requestAnimationFrame */ /* global Node, requestAnimationFrame */ // eslint-disable-line
import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
import { RemixUiTerminal } from '@remix-ui/terminal' // eslint-disable-line
import { Plugin } from '@remixproject/engine' import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
import * as remixBleach from '../../lib/remixBleach' const vm = require('vm')
const EventManager = require('../../lib/events')
var yo = require('yo-yo') const CommandInterpreterAPI = require('../../lib/cmdInterpreterAPI')
var javascriptserialize = require('javascript-serialize') const AutoCompletePopup = require('../ui/auto-complete-popup')
var jsbeautify = require('js-beautify')
var type = require('component-type')
var vm = require('vm')
var EventManager = require('../../lib/events')
var CommandInterpreterAPI = require('../../lib/cmdInterpreterAPI') import { CompilerImports } from '@remix-project/core-plugin' // eslint-disable-line
var AutoCompletePopup = require('../ui/auto-complete-popup') const globalRegistry = require('../../global/registry')
var TxLogger = require('../../app/ui/txLogger') const GistHandler = require('../../lib/gist-handler')
var csjs = require('csjs-inject') const KONSOLES = []
var css = require('./styles/terminal-styles')
var KONSOLES = []
function register (api) { KONSOLES.push(api) } function register (api) { KONSOLES.push(api) }
var ghostbar = yo`<div class=${css.ghostbar} bg-secondary></div>`
const profile = { const profile = {
displayName: 'Terminal', displayName: 'Terminal',
name: 'terminal', name: 'terminal',
@ -36,75 +30,68 @@ const profile = {
class Terminal extends Plugin { class Terminal extends Plugin {
constructor (opts, api) { constructor (opts, api) {
super(profile) super(profile)
var self = this this.fileImport = new CompilerImports()
self.event = new EventManager() this.gistHandler = new GistHandler()
self.blockchain = opts.blockchain this.event = new EventManager()
self._api = api this.globalRegistry = globalRegistry
self._opts = opts this.element = document.createElement('div')
self.data = { this.element.setAttribute('class', 'panel')
this.element.setAttribute('id', 'terminal-view')
this.element.setAttribute('data-id', 'terminalContainer-view')
this.eventsDecoder = this.globalRegistry.get('eventsDecoder').api
this.txListener = this.globalRegistry.get('txlistener').api
this._deps = {
fileManager: this.globalRegistry.get('filemanager').api,
editor: this.globalRegistry.get('editor').api,
compilersArtefacts: this.globalRegistry.get('compilersartefacts').api,
offsetToLineColumnConverter: this.globalRegistry.get('offsettolinecolumnconverter').api
}
this.commandHelp = {
'remix.loadgist(id)': 'Load a gist in the file explorer.',
'remix.loadurl(url)': 'Load the given url in the file explorer. The url can be of type github, swarm, ipfs or raw http',
'remix.execute(filepath)': 'Run the script specified by file path. If filepath is empty, script currently displayed in the editor is executed.',
'remix.exeCurrent()': 'Run the script currently displayed in the editor',
'remix.help()': 'Display this help message'
}
this.blockchain = opts.blockchain
this.vm = vm
this._api = api
this._opts = opts
this.config = this.globalRegistry.get('config').api
this.version = packageJson.version
this.data = {
lineLength: opts.lineLength || 80, // ???? lineLength: opts.lineLength || 80, // ????
session: [], session: [],
activeFilters: { commands: {}, input: '' }, activeFilters: { commands: {}, input: '' },
filterFns: {} filterFns: {}
} }
self._view = { el: null, bar: null, input: null, term: null, journal: null, cli: null } this._view = { el: null, bar: null, input: null, term: null, journal: null, cli: null }
self._components = {} this._components = {}
self._components.cmdInterpreter = new CommandInterpreterAPI(this, null, self.blockchain) this._components.cmdInterpreter = new CommandInterpreterAPI(this, null, this.blockchain)
self._components.autoCompletePopup = new AutoCompletePopup(self._opts) this._components.autoCompletePopup = new AutoCompletePopup(this._opts)
self._components.autoCompletePopup.event.register('handleSelect', function (input) { this._commands = {}
const textList = self._view.input.innerText.split(' ') this.commands = {}
textList.pop() this._JOURNAL = []
textList.push(input) this._jobs = []
self._view.input.innerText = textList this._INDEX = {}
self._view.input.focus() this._INDEX.all = []
self.putCursor2End(self._view.input) this._INDEX.allMain = []
this._INDEX.commands = {}
this._INDEX.commandsMain = {}
if (opts.shell) this._shell = opts.shell // ???
register(this)
this.event.register('debuggingRequested', async (hash) => {
// TODO should probably be in the run module
if (!await this._opts.appManager.isActive('debugger')) await this._opts.appManager.activatePlugin('debugger')
this.call('menuicons', 'select', 'debugger')
this.call('debugger', 'debug', hash)
}) })
self._commands = {} this.logHtmlResponse = []
self.commands = {} this.logResponse = []
self._JOURNAL = []
self._jobs = []
self._INDEX = {}
self._INDEX.all = []
self._INDEX.allMain = []
self._INDEX.commands = {}
self._INDEX.commandsMain = {}
self.registerCommand('html', self._blocksRenderer('html'), { activate: true })
self.registerCommand('log', self._blocksRenderer('log'), { activate: true })
self.registerCommand('info', self._blocksRenderer('info'), { activate: true })
self.registerCommand('warn', self._blocksRenderer('warn'), { activate: true })
self.registerCommand('error', self._blocksRenderer('error'), { activate: true })
self.registerCommand('script', function execute (args, scopedCommands, append) {
var script = String(args[0])
self._shell(script, scopedCommands, function (error, output) {
if (error) scopedCommands.error(error)
else if (output) scopedCommands.log(output)
})
}, { activate: true })
function basicFilter (value, query) { try { return value.indexOf(query) !== -1 } catch (e) { return false } }
self.registerFilter('log', basicFilter)
self.registerFilter('info', basicFilter)
self.registerFilter('warn', basicFilter)
self.registerFilter('error', basicFilter)
self.registerFilter('script', basicFilter)
if (opts.shell) self._shell = opts.shell // ???
register(self)
} }
onActivation () { onActivation () {
this.on('scriptRunner', 'log', (msg) => { this.renderComponent()
this.commands.log.apply(this.commands, msg.data)
})
this.on('scriptRunner', 'info', (msg) => {
this.commands.info.apply(this.commands, msg.data)
})
this.on('scriptRunner', 'warn', (msg) => {
this.commands.warn.apply(this.commands, msg.data)
})
this.on('scriptRunner', 'error', (msg) => {
this.commands.error.apply(this.commands, msg.data)
})
} }
onDeactivation () { onDeactivation () {
@ -114,690 +101,43 @@ class Terminal extends Plugin {
this.off('scriptRunner', 'error') this.off('scriptRunner', 'error')
} }
log (message) {
var command = this.commands[message.type]
if (typeof command === 'function') {
if (typeof message.value === 'string' && message.type === 'html') {
var el = document.createElement('div')
el.innerHTML = remixBleach.sanitize(message.value, {
list: [
'a',
'b',
'p',
'em',
'strong',
'div',
'span',
'ul',
'li',
'ol',
'hr'
]
})
message.value = el
}
command(message.value)
};
}
logHtml (html) { logHtml (html) {
var command = this.commands.html this.logHtmlResponse.push(html.innerText)
if (typeof command === 'function') command(html) this.renderComponent()
this.resetLogHtml()
} }
focus () { resetLogHtml () {
if (this._view.input) this._view.input.focus() this.logHtmlResponse = []
} }
render () { log (message) {
var self = this this.logResponse.push(message)
if (self._view.el) return self._view.el this.renderComponent()
self._view.journal = yo`<div id="journal" class=${css.journal} data-id="terminalJournal"></div>` this.resetLog()
self._view.input = yo`
<span class=${css.input} onload=${() => { this.focus() }} onpaste=${paste} onkeydown=${change}></span>
`
self._view.input.setAttribute('spellcheck', 'false')
self._view.input.setAttribute('contenteditable', 'true')
self._view.input.setAttribute('id', 'terminalCliInput')
self._view.input.setAttribute('data-id', 'terminalCliInput')
self._view.input.innerText = '\n'
self._view.cli = yo`
<div id="terminalCli" data-id="terminalCli" class="${css.cli}" onclick=${focusinput}>
<span class=${css.prompt}>${'>'}</span>
${self._view.input}
</div>
`
self._view.icon = yo`
<i onmouseenter=${hover} onmouseleave=${hover} onmousedown=${minimize}
class="mx-2 ${css.toggleTerminal} fas fa-angle-double-down" data-id="terminalToggleIcon"></i>`
self._view.dragbar = yo`
<div onmousedown=${mousedown} class=${css.dragbarHorizontal}></div>`
self._view.pendingTxCount = yo`<div class="mx-2" title='Pending Transactions'>0</div>`
self._view.inputSearch = yo`<input
spellcheck="false"
type="text"
class="border ${css.filter} form-control"
id="searchInput"
onkeydown=${filter}
placeholder="Search with transaction hash or address"
data-id="terminalInputSearch">
</input>`
self._view.bar = yo`
<div class="${css.bar}">
${self._view.dragbar}
<div class="${css.menu} border-top border-dark bg-light" data-id="terminalToggleMenu">
${self._view.icon}
<div class="mx-2" id="clearConsole" data-id="terminalClearConsole" onclick=${clear}>
<i class="fas fa-ban" aria-hidden="true" title="Clear console"
onmouseenter=${hover} onmouseleave=${hover}></i>
</div>
${self._view.pendingTxCount}
<div class=${css.verticalLine}></div>
<div class="pt-1 h-80 mx-3 align-items-center ${css.listenOnNetwork} custom-control custom-checkbox">
<input
class="custom-control-input"
id="listenNetworkCheck"
onchange=${listenOnNetwork}
type="checkbox"
title="If checked Remix will listen on all transactions mined in the current environment and not only transactions created by you"
>
<label
class="pt-1 form-check-label custom-control-label text-nowrap""
title="If checked Remix will listen on all transactions mined in the current environment and not only transactions created by you"
for="listenNetworkCheck"
>
listen on network
</label>
</div>
<div class=${css.search}>
<i class="fas fa-search ${css.searchIcon} bg-light" aria-hidden="true"></i>
${self._view.inputSearch}
</div>
</div>
</div>
`
self._view.term = yo`
<div class="${css.terminal_container}" tabindex="-1" data-id="terminalContainer" onscroll=${throttle(reattach, 10)} onkeydown=${focusinput}>
${self._components.autoCompletePopup.render()}
<div data-id="terminalContainerDisplay" style="
position: absolute;
height: 100%;
width: 100%;
opacity: 0.1;
z-index: -1;
"></div>
<div class=${css.terminal}>
${self._view.journal}
${self._view.cli}
</div>
</div>
`
self._view.el = yo`
<div class="${css.panel}" style="height: 180px;">
${self._view.bar}
${self._view.term}
</div>
`
setInterval(async () => {
try {
self._view.pendingTxCount.innerHTML = await self.call('udapp', 'pendingTransactionsCount')
} catch (err) {}
}, 1000)
function listenOnNetwork (ev) {
self.event.trigger('listenOnNetWork', [ev.currentTarget.checked])
}
function paste (event) {
const selection = window.getSelection()
if (!selection.rangeCount) return false
event.preventDefault()
event.stopPropagation()
var clipboard = (event.clipboardData || window.clipboardData)
var text = clipboard.getData('text/plain')
text = text.replace(/[^\x20-\xFF]/gi, '') // remove non-UTF-8 characters
var temp = document.createElement('div')
temp.innerHTML = text
var textnode = document.createTextNode(temp.textContent)
selection.getRangeAt(0).insertNode(textnode)
selection.empty()
self.scroll2bottom()
placeCaretAtEnd(event.currentTarget)
}
function placeCaretAtEnd (el) {
el.focus()
var range = document.createRange()
range.selectNodeContents(el)
range.collapse(false)
var sel = window.getSelection()
sel.removeAllRanges()
sel.addRange(range)
}
function throttle (fn, wait) {
var time = Date.now()
return function debounce () {
if ((time + wait - Date.now()) < 0) {
fn.apply(this, arguments)
time = Date.now()
}
}
}
var css2 = csjs`
.anchor {
position : static;
border-top : 2px dotted blue;
height : 10px;
}
.overlay {
position : absolute;
width : 100%;
display : flex;
align-items : center;
justify-content : center;
bottom : 0;
right : 15px;
min-height : 20px;
}
.text {
z-index : 2;
color : black;
font-weight : bold;
pointer-events : none;
}
.background {
z-index : 1;
opacity : 0.8;
background-color : #a6aeba;
cursor : pointer;
}
.ul {
padding-left : 20px;
padding-bottom : 5px;
}
`
var text = yo`<div class="${css2.overlay} ${css2.text}"></div>`
var background = yo`<div class="${css2.overlay} ${css2.background}"></div>`
var placeholder = yo`<div class=${css2.anchor}>${background}${text}</div>`
var inserted = false
window.addEventListener('resize', function (event) {
self.event.trigger('resize', [])
self.event.trigger('resize', [])
})
function focusinput (event) {
if (
event.altKey ||
event.ctrlKey ||
event.metaKey ||
event.shiftKey ||
event.key === 'Down' ||
event.key === 'ArrowDown' ||
event.key === 'Up' ||
event.key === 'ArrowUp' ||
event.key === 'Left' ||
event.key === 'ArrowLeft' ||
event.key === 'Right' ||
event.key === 'ArrowRight' ||
event.key === 'Esc' ||
event.key === 'Escape'
) return
refocus()
}
function refocus () {
self._view.input.focus()
reattach({ currentTarget: self._view.term })
delete self.scroll2bottom
self.scroll2bottom()
}
function reattach (event) {
var el = event.currentTarget
var isBottomed = el.scrollHeight - el.scrollTop - el.clientHeight < 30
if (isBottomed) {
if (inserted) {
text.innerText = ''
background.onclick = undefined
if (placeholder.parentElement) self._view.journal.removeChild(placeholder)
}
inserted = false
delete self.scroll2bottom
} else {
if (!inserted) self._view.journal.appendChild(placeholder)
inserted = true
check()
if (!placeholder.nextElementSibling) {
placeholder.style.display = 'none'
} else {
placeholder.style = ''
}
self.scroll2bottom = function () {
var next = placeholder.nextElementSibling
if (next) {
placeholder.style = ''
check()
var messages = 1
while ((next = next.nextElementSibling)) messages += 1
text.innerText = `${messages} new unread log entries`
} else {
placeholder.style.display = 'none'
}
}
}
}
function check () {
var pos1 = self._view.term.offsetHeight + self._view.term.scrollTop - (self._view.el.offsetHeight * 0.15)
var pos2 = placeholder.offsetTop
if ((pos1 - pos2) > 0) {
text.style.display = 'none'
background.style.position = 'relative'
background.style.opacity = 0.3
background.style.right = 0
background.style.borderBox = 'content-box'
background.style.padding = '2px'
background.style.height = (self._view.journal.offsetHeight - (placeholder.offsetTop + placeholder.offsetHeight)) + 'px'
background.onclick = undefined
background.style.cursor = 'default'
background.style.pointerEvents = 'none'
} else {
background.style = ''
text.style = ''
background.onclick = function (event) {
placeholder.scrollIntoView()
check()
}
}
}
function hover (event) { event.currentTarget.classList.toggle(css.hover) }
function minimize (event) {
event.preventDefault()
event.stopPropagation()
if (event.button === 0) {
var classList = self._view.icon.classList
classList.toggle('fa-angle-double-down')
classList.toggle('fa-angle-double-up')
self.event.trigger('resize', [])
}
}
var filtertimeout = null
function filter (event) {
if (filtertimeout) {
clearTimeout(filtertimeout)
}
filtertimeout = setTimeout(() => {
self.updateJournal({ type: 'search', value: self._view.inputSearch.value })
self.scroll2bottom()
}, 500)
}
function clear (event) {
refocus()
self._view.journal.innerHTML = ''
}
// ----------------- resizeable ui ---------------
function mousedown (event) {
event.preventDefault()
if (event.which === 1) {
moveGhostbar(event)
document.body.appendChild(ghostbar)
document.addEventListener('mousemove', moveGhostbar)
document.addEventListener('mouseup', removeGhostbar)
document.addEventListener('keydown', cancelGhostbar)
}
}
function cancelGhostbar (event) {
if (event.keyCode === 27) {
document.body.removeChild(ghostbar)
document.removeEventListener('mousemove', moveGhostbar)
document.removeEventListener('mouseup', removeGhostbar)
document.removeEventListener('keydown', cancelGhostbar)
}
}
function moveGhostbar (event) { // @NOTE HORIZONTAL ghostbar
ghostbar.style.top = self._api.getPosition(event) + 'px'
}
function removeGhostbar (event) {
if (self._view.icon.classList.contains('fa-angle-double-up')) {
self._view.icon.classList.toggle('fa-angle-double-down')
self._view.icon.classList.toggle('fa-angle-double-up')
}
document.body.removeChild(ghostbar)
document.removeEventListener('mousemove', moveGhostbar)
document.removeEventListener('mouseup', removeGhostbar)
document.removeEventListener('keydown', cancelGhostbar)
self.event.trigger('resize', [self._api.getPosition(event)])
}
self._cmdHistory = []
self._cmdIndex = -1
self._cmdTemp = ''
var intro = yo`
<div><div> - Welcome to Remix ${packageJson.version} - </div><br>
<div>You can use this terminal to: </div>
<ul class=${css2.ul}>
<li>Check transactions details and start debugging.</li>
<li>Execute JavaScript scripts:
<br />
<i> - Input a script directly in the command line interface </i>
<br />
<i> - Select a Javascript file in the file explorer and then run \`remix.execute()\` or \`remix.exeCurrent()\` in the command line interface </i>
<br />
<i> - Right click on a JavaScript file in the file explorer and then click \`Run\` </i>
</li>
</ul>
<div>The following libraries are accessible:</div>
<ul class=${css2.ul}>
<li><a target="_blank" href="https://web3js.readthedocs.io/en/1.0/">web3 version 1.0.0</a></li>
<li><a target="_blank" href="https://docs.ethers.io">ethers.js</a> </li>
<li><a target="_blank" href="https://www.npmjs.com/package/swarmgw">swarmgw</a> </li>
<li>remix (run remix.help() for more info)</li>
</ul>
</div>
`
self._shell('remix.help()', self.commands, () => {})
self.commands.html(intro)
self._components.txLogger = new TxLogger(this, self.blockchain)
self._components.txLogger.event.register('debuggingRequested', async (hash) => {
// TODO should probably be in the run module
if (!await self._opts.appManager.isActive('debugger')) await self._opts.appManager.activatePlugin('debugger')
this.call('menuicons', 'select', 'debugger')
this.call('debugger', 'debug', hash)
})
return self._view.el
function wrapScript (script) {
const isKnownScript = ['remix.', 'git'].some(prefix => script.trim().startsWith(prefix))
if (isKnownScript) return script
return `
try {
const ret = ${script};
if (ret instanceof Promise) {
ret.then((result) => { console.log(result) }).catch((error) => { console.log(error) })
} else {
console.log(ret)
}
} catch (e) {
console.log(e.message)
}
`
}
function change (event) {
if (self._components.autoCompletePopup.handleAutoComplete(
event,
self._view.input.innerText)) return
if (self._view.input.innerText.length === 0) self._view.input.innerText += '\n'
if (event.which === 13) {
if (event.ctrlKey) { // <ctrl+enter>
self._view.input.innerText += '\n'
self.putCursor2End(self._view.input)
self.scroll2bottom()
self._components.autoCompletePopup.removeAutoComplete()
} else { // <enter>
self._cmdIndex = -1
self._cmdTemp = ''
event.preventDefault()
var script = self._view.input.innerText.trim()
self._view.input.innerText = '\n'
if (script.length) {
self._cmdHistory.unshift(script)
self.commands.script(wrapScript(script))
}
self._components.autoCompletePopup.removeAutoComplete()
}
} else if (event.which === 38) { // <arrowUp>
var len = self._cmdHistory.length
if (len === 0) return event.preventDefault()
if (self._cmdHistory.length - 1 > self._cmdIndex) {
self._cmdIndex++
}
self._view.input.innerText = self._cmdHistory[self._cmdIndex]
self.putCursor2End(self._view.input)
self.scroll2bottom()
} else if (event.which === 40) { // <arrowDown>
if (self._cmdIndex > -1) {
self._cmdIndex--
}
self._view.input.innerText = self._cmdIndex >= 0 ? self._cmdHistory[self._cmdIndex] : self._cmdTemp
self.putCursor2End(self._view.input)
self.scroll2bottom()
} else {
self._cmdTemp = self._view.input.innerText
}
}
} }
putCursor2End (editable) { resetLog () {
var range = document.createRange() this.logResponse = []
range.selectNode(editable)
var child = editable
var chars
while (child) {
if (child.lastChild) child = child.lastChild
else break
if (child.nodeType === Node.TEXT_NODE) {
chars = child.textContent.length
} else {
chars = child.innerHTML.length
}
}
range.setEnd(child, chars)
var toStart = true
var toEnd = !toStart
range.collapse(toEnd)
var sel = window.getSelection()
sel.removeAllRanges()
sel.addRange(range)
editable.focus()
} }
updateJournal (filterEvent) { render () {
var self = this return this.element
var commands = self.data.activeFilters.commands
var value = filterEvent.value
if (filterEvent.type === 'select') {
commands[value] = true
if (!self._INDEX.commandsMain[value]) return
self._INDEX.commandsMain[value].forEach(item => {
item.root.steps.forEach(item => { self._JOURNAL[item.gidx] = item })
self._JOURNAL[item.gidx] = item
})
} else if (filterEvent.type === 'deselect') {
commands[value] = false
if (!self._INDEX.commandsMain[value]) return
self._INDEX.commandsMain[value].forEach(item => {
item.root.steps.forEach(item => { self._JOURNAL[item.gidx].hide = true })
self._JOURNAL[item.gidx].hide = true
})
} else if (filterEvent.type === 'search') {
if (value !== self.data.activeFilters.input) {
var query = self.data.activeFilters.input = value
var items = self._JOURNAL
for (var gidx = 0, len = items.length; gidx < len; gidx++) {
var item = items[gidx]
if (item && self.data.filterFns[item.cmd]) {
var show = query.length ? self.data.filterFns[item.cmd](item.args, query) : true
item.hide = !show
}
}
}
}
var df = document.createDocumentFragment()
self._JOURNAL.forEach(item => {
if (item && item.el && !item.hide) df.appendChild(item.el)
})
self._view.journal.innerHTML = ''
requestAnimationFrame(function updateDOM () {
self._view.journal.appendChild(df)
})
} }
_appendItem (item) { renderComponent () {
var self = this ReactDOM.render(
var { el, gidx } = item <RemixUiTerminal
self._JOURNAL[gidx] = item plugin = {this}
if (!self._jobs.length) { />,
requestAnimationFrame(function updateTerminal () { this.element
self._jobs.forEach(el => self._view.journal.appendChild(el)) )
self.scroll2bottom()
self._jobs = []
})
}
if (self.data.activeFilters.commands[item.cmd]) self._jobs.push(el)
} }
scroll2bottom () { scroll2bottom () {
var self = this
setTimeout(function () { setTimeout(function () {
self._view.term.scrollTop = self._view.term.scrollHeight
}, 0) }, 0)
} }
_blocksRenderer (mode) {
if (mode === 'html') {
return function logger (args, scopedCommands, append) {
if (args.length) append(args[0])
}
}
mode = {
log: 'text-info',
info: 'text-info',
warn: 'text-warning',
error: 'text-danger'
}[mode] // defaults
if (mode) {
const filterUndefined = (el) => el !== undefined && el !== null
return function logger (args, scopedCommands, append) {
var types = args.filter(filterUndefined).map(type)
var values = javascriptserialize.apply(null, args.filter(filterUndefined)).map(function (val, idx) {
if (typeof args[idx] === 'string') {
const el = document.createElement('div')
el.innerHTML = args[idx].replace(/(\r\n|\n|\r)/gm, '<br>')
val = el.children.length === 0 ? el.firstChild : el
}
if (types[idx] === 'element') val = jsbeautify.html(val)
return val
})
if (values.length) {
append(yo`<span class="${mode}" >${values}</span>`)
}
}
} else {
throw new Error('mode is not supported')
}
}
_scopeCommands (append) {
var self = this
var scopedCommands = {}
Object.keys(self.commands).forEach(function makeScopedCommand (cmd) {
var command = self._commands[cmd]
scopedCommands[cmd] = function _command () {
var args = [...arguments]
command(args, scopedCommands, el => append(cmd, args, blockify(el)))
}
})
return scopedCommands
}
registerFilter (commandName, filterFn) {
this.data.filterFns[commandName] = filterFn
}
registerCommand (name, command, opts) {
var self = this
name = String(name)
if (self._commands[name]) throw new Error(`command "${name}" exists already`)
if (typeof command !== 'function') throw new Error(`invalid command: ${command}`)
self._commands[name] = command
self._INDEX.commands[name] = []
self._INDEX.commandsMain[name] = []
self.commands[name] = function _command () {
var args = [...arguments]
var steps = []
var root = { steps, cmd: name }
var ITEM = { root, cmd: name }
root.gidx = self._INDEX.allMain.push(ITEM) - 1
root.idx = self._INDEX.commandsMain[name].push(ITEM) - 1
function append (cmd, params, el) {
var item
if (cmd) { // subcommand
item = { el, cmd, root }
} else { // command
item = ITEM
item.el = el
cmd = name
}
item.gidx = self._INDEX.all.push(item) - 1
item.idx = self._INDEX.commands[cmd].push(item) - 1
item.step = steps.push(item) - 1
item.args = params
self._appendItem(item)
}
var scopedCommands = self._scopeCommands(append)
command(args, scopedCommands, el => append(null, args, blockify(el)))
}
var help = typeof command.help === 'string' ? command.help : [
'// no help available for:',
`terminal.commands.${name}(...)`
].join('\n')
self.commands[name].toString = _ => { return help }
self.commands[name].help = help
self.data.activeFilters.commands[name] = opts && opts.activate
if (opts.filterFn) {
self.registerFilter(name, opts.filterFn)
}
return self.commands[name]
}
async _shell (script, scopedCommands, done) { // default shell
if (script.indexOf('remix:') === 0) {
return done(null, 'This type of command has been deprecated and is not functionning anymore. Please run remix.help() to list available commands.')
}
var self = this
if (script.indexOf('remix.') === 0) {
// we keep the old feature. This will basically only be called when the command is querying the "remix" object.
// for all the other case, we use the Code Executor plugin
var context = domTerminalFeatures(self, scopedCommands, self.blockchain)
try {
var cmds = vm.createContext(context)
var result = vm.runInContext(script, cmds)
return done(null, result)
} catch (error) {
return done(error.message)
}
}
try {
let result
if (script.trim().startsWith('git')) {
// result = await this.call('git', 'execute', script)
} else {
result = await this.call('scriptRunner', 'execute', script)
}
if (result) self.commands.html(yo`<pre>${result}</pre>`)
done()
} catch (error) {
done(error.message || error)
}
}
} }
function domTerminalFeatures (self, scopedCommands, blockchain) {
return {
remix: self._components.cmdInterpreter
}
}
function blockify (el) { return yo`<div class="px-4 ${css.block}" data-id="block_${el.getAttribute ? el.getAttribute('id') : ''}">${el}</div>` }
module.exports = Terminal module.exports = Terminal

@ -39,7 +39,11 @@ class AnalysisTab extends ViewPlugin {
} }
} }
onActivation () { async onActivation () {
const isSolidityActive = await this.call('manager', 'isActive', 'solidity')
if (!isSolidityActive) {
await this.call('manager', 'activatePlugin', 'solidity')
}
this.renderComponent() this.renderComponent()
} }

@ -115,16 +115,22 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA
async onActivation () { async onActivation () {
super.onActivation() super.onActivation()
this.call('filePanel', 'registerContextMenuItem', { this.on('filePanel', 'workspaceInitializationCompleted', () => {
id: 'solidity', this.call('filePanel', 'registerContextMenuItem', {
name: 'compileFile', id: 'solidity',
label: 'Compile', name: 'compileFile',
type: [], label: 'Compile',
extension: ['.sol'], type: [],
path: [], extension: ['.sol'],
pattern: [] path: [],
pattern: []
})
}) })
this.currentFile = await this.call('fileManager', 'file') try {
this.currentFile = await this.call('fileManager', 'file')
} catch (error) {
if (error.message !== 'Error: No such file or directory No file selected') throw error
}
} }
getCompilerParameters () { getCompilerParameters () {

@ -218,11 +218,12 @@ class ContractDropdownUI {
if (this.selectContractNames.value === '') this.enableAtAddress(false) if (this.selectContractNames.value === '') this.enableAtAddress(false)
} else { } else {
this.loadType = 'other' this.loadType = 'other'
this.createPanel.style.display = 'none' this.createPanel.style.display = 'block'
this.orLabel.style.display = 'none' this.orLabel.style.display = 'block'
this.compFails.style.display = 'none' this.contractNamesContainer.style.display = 'block'
this.contractNamesContainer.style.display = 'none' this.selectContractNames.style.display = 'block'
this.abiLabel.style.display = 'none' this.abiLabel.style.display = 'none'
if (this.selectContractNames.value === '') this.enableAtAddress(false)
} }
} }

@ -170,10 +170,10 @@ class SettingsUI {
onchange=${() => this.validateValue()} onchange=${() => this.validateValue()}
> >
<select name="unit" class="form-control p-1 ${css.gasNvalUnit} ${css.col2_2} custom-select" id="unit"> <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="wei">Wei</option>
<option data-unit="gwei">gwei</option> <option data-unit="gwei">Gwei</option>
<option data-unit="finney">finney</option> <option data-unit="finney">Finney</option>
<option data-unit="ether">ether</option> <option data-unit="ether">Ether</option>
</select> </select>
</div> </div>
</div> </div>

@ -74,6 +74,14 @@ module.exports = class TestTab extends ViewPlugin {
} }
} }
async onActivation () {
const isSolidityActive = await this.call('manager', 'isActive', 'solidity')
if (!isSolidityActive) {
await this.call('manager', 'activatePlugin', 'solidity')
}
this.updateRunAction()
}
onDeactivation () { onDeactivation () {
this.off('filePanel', 'newTestFileCreated') this.off('filePanel', 'newTestFileCreated')
this.off('filePanel', 'setWorkspace') this.off('filePanel', 'setWorkspace')
@ -215,7 +223,7 @@ module.exports = class TestTab extends ViewPlugin {
runningTests[fileName].content runningTests[fileName].content
) )
await this.call('editor', 'discardHighlight') await this.call('editor', 'discardHighlight')
await this.call('editor', 'highlight', location, fileName) await this.call('editor', 'highlight', location, fileName, '', { focus: true })
} }
} }
@ -247,6 +255,14 @@ module.exports = class TestTab extends ViewPlugin {
testCallback (result, runningTests) { testCallback (result, runningTests) {
this.testsOutput.hidden = false this.testsOutput.hidden = false
let debugBtn = yo``
if ((result.type === 'testPass' || result.type === 'testFailure') && result.debugTxHash) {
const { web3, debugTxHash } = result
debugBtn = yo`<div id=${result.value.replaceAll(' ', '_')} class="btn border btn btn-sm ml-1" title="Start debugging" onclick=${() => this.startDebug(debugTxHash, web3)}>
<i class="fas fa-bug"></i>
</div>`
debugBtn.style.cursor = 'pointer'
}
if (result.type === 'contract') { if (result.type === 'contract') {
this.testSuite = result.value this.testSuite = result.value
if (this.testSuites) { if (this.testSuites) {
@ -268,29 +284,18 @@ module.exports = class TestTab extends ViewPlugin {
<div <div
id="${this.runningTestFileName}" id="${this.runningTestFileName}"
data-id="testTabSolidityUnitTestsOutputheader" data-id="testTabSolidityUnitTestsOutputheader"
class="${css.testPass} ${css.testLog} bg-light mb-2 text-success border-0" class="${css.testPass} ${css.testLog} bg-light mb-2 px-2 text-success border-0"
onclick=${() => this.discardHighlight()} onclick=${() => this.discardHighlight()}
> >
${result.value} <div class="d-flex my-1 align-items-start justify-content-between">
<span style="margin-block: auto" > ${result.value}</span>
${debugBtn}
</div>
</div> </div>
`) `)
} else if (result.type === 'testFailure') { } else if (result.type === 'testFailure') {
if (result.hhLogs && result.hhLogs.length) this.printHHLogs(result.hhLogs, result.value) if (result.hhLogs && result.hhLogs.length) this.printHHLogs(result.hhLogs, result.value)
if (!result.assertMethod) { if (!result.assertMethod) {
let debugBtn = yo``
if (result.errMsg.includes('Transaction has been reverted by the EVM')) {
const txHash = JSON.parse(result.errMsg.replace('Transaction has been reverted by the EVM:', '')).transactionHash
const { web3 } = result
debugBtn = yo`<div
class="btn border btn btn-sm ml-1"
title="Start debugging"
onclick=${() => this.startDebug(txHash, web3)}
>
<i class="fas fa-bug"></i>
</div>`
debugBtn.style.visibility = 'visible'
debugBtn.style.cursor = 'pointer'
} else debugBtn.style.visibility = 'hidden'
this.testsOutput.appendChild(yo` this.testsOutput.appendChild(yo`
<div <div
class="bg-light mb-2 px-2 ${css.testLog} d-flex flex-column text-danger border-0" class="bg-light mb-2 px-2 ${css.testLog} d-flex flex-column text-danger border-0"
@ -315,7 +320,10 @@ module.exports = class TestTab extends ViewPlugin {
id="UTContext${result.context}" id="UTContext${result.context}"
onclick=${() => this.highlightLocation(result.location, runningTests, result.filename)} onclick=${() => this.highlightLocation(result.location, runningTests, result.filename)}
> >
<span> ${result.value}</span> <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="text-dark">Error Message:</span>
<span class="pb-2 text-break">"${result.errMsg}"</span> <span class="pb-2 text-break">"${result.errMsg}"</span>
<span class="text-dark">Assertion:</span> <span class="text-dark">Assertion:</span>
@ -499,7 +507,7 @@ module.exports = class TestTab extends ViewPlugin {
usingWorker: canUseWorker(currentVersion), usingWorker: canUseWorker(currentVersion),
runs runs
} }
this.testRunner.runTestSources(runningTest, compilerConfig, () => {}, () => {}, (error, result) => { this.testRunner.runTestSources(runningTest, compilerConfig, () => {}, () => {}, null, (error, result) => {
if (error) return reject(error) if (error) return reject(error)
resolve(result) resolve(result)
}, (url, cb) => { }, (url, cb) => {
@ -527,17 +535,22 @@ module.exports = class TestTab extends ViewPlugin {
usingWorker: canUseWorker(currentVersion), usingWorker: canUseWorker(currentVersion),
runs runs
} }
const deployCb = async (file, contractAddress) => {
const compilerData = await this.call('compilerArtefacts', 'getCompilerAbstract', file)
await this.call('compilerArtefacts', 'addResolvedContract', contractAddress, compilerData)
}
this.testRunner.runTestSources( this.testRunner.runTestSources(
runningTests, runningTests,
compilerConfig, compilerConfig,
(result) => this.testCallback(result, runningTests), (result) => this.testCallback(result, runningTests),
(_err, result, cb) => this.resultsCallback(_err, result, cb), (_err, result, cb) => this.resultsCallback(_err, result, cb),
deployCb,
(error, result) => { (error, result) => {
this.updateFinalResult(error, result, testFilePath) this.updateFinalResult(error, result, testFilePath)
callback(error) callback(error)
}, (url, cb) => { }, (url, cb) => {
return this.contentImport.resolveAndSave(url).then((result) => cb(null, result)).catch((error) => cb(error.message)) return this.contentImport.resolveAndSave(url).then((result) => cb(null, result)).catch((error) => cb(error.message))
} }, { testFilePath }
) )
}).catch((error) => { }).catch((error) => {
if (error) return // eslint-disable-line if (error) return // eslint-disable-line
@ -632,7 +645,6 @@ module.exports = class TestTab extends ViewPlugin {
el.setAttribute('title', 'No solidity file selected') el.setAttribute('title', 'No solidity file selected')
} else { } else {
el.setAttribute('title', 'The "Solidity Plugin" should be activated') el.setAttribute('title', 'The "Solidity Plugin" should be activated')
// @todo(#2747) we can activate the plugin here
} }
} }
if (!this.runActionElement) { if (!this.runActionElement) {

@ -21,7 +21,7 @@ const themes = [
const profile = { const profile = {
name: 'theme', name: 'theme',
events: ['themeChanged'], events: ['themeChanged'],
methods: ['switchTheme', 'getThemes', 'currentTheme'], methods: ['switchTheme', 'getThemes', 'currentTheme', 'fixInvert'],
version: packageJson.version, version: packageJson.version,
kind: 'theme' kind: 'theme'
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

@ -76,7 +76,6 @@ export class ExecutionContext {
if (err) name = 'Unknown' if (err) name = 'Unknown'
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
else if (id === 1) name = 'Main' else if (id === 1) name = 'Main'
else if (id === 2) name = 'Morden (deprecated)'
else if (id === 3) name = 'Ropsten' else if (id === 3) name = 'Ropsten'
else if (id === 4) name = 'Rinkeby' else if (id === 4) name = 'Rinkeby'
else if (id === 5) name = 'Goerli' else if (id === 5) name = 'Goerli'

@ -20,7 +20,6 @@ class VMProvider {
this.accounts = {} this.accounts = {}
this.RemixSimulatorProvider = new Provider({ fork: this.executionContext.getCurrentFork() }) this.RemixSimulatorProvider = new Provider({ fork: this.executionContext.getCurrentFork() })
this.RemixSimulatorProvider.init() this.RemixSimulatorProvider.init()
this.RemixSimulatorProvider.Accounts.resetAccounts()
this.web3 = new Web3(this.RemixSimulatorProvider) this.web3 = new Web3(this.RemixSimulatorProvider)
extend(this.web3) extend(this.web3)
this.accounts = {} this.accounts = {}

@ -6,7 +6,6 @@ var EventManager = require('../lib/events')
var toolTip = require('../app/ui/tooltip') var toolTip = require('../app/ui/tooltip')
var globalRegistry = require('../global/registry') var globalRegistry = require('../global/registry')
var SourceHighlighter = require('../app/editor/sourceHighlighter')
var GistHandler = require('./gist-handler') var GistHandler = require('./gist-handler')
class CmdInterpreterAPI { class CmdInterpreterAPI {
@ -17,7 +16,6 @@ class CmdInterpreterAPI {
self._components = {} self._components = {}
self._components.registry = localRegistry || globalRegistry self._components.registry = localRegistry || globalRegistry
self._components.terminal = terminal self._components.terminal = terminal
self._components.sourceHighlighter = new SourceHighlighter()
self._components.fileImport = new CompilerImports() self._components.fileImport = new CompilerImports()
self._components.gistHandler = new GistHandler() self._components.gistHandler = new GistHandler()
self._deps = { self._deps = {
@ -93,7 +91,6 @@ class CmdInterpreterAPI {
if (cb) cb() if (cb) cb()
return return
} }
self._components.terminal.commands.script(content) self._components.terminal.commands.script(content)
} }

@ -14,7 +14,7 @@ const requiredModules = [ // services + layout views + system views
const dependentModules = ['git', 'hardhat'] // module which shouldn't be manually activated (e.g git is activated by remixd) const dependentModules = ['git', 'hardhat'] // module which shouldn't be manually activated (e.g git is activated by remixd)
export function isNative (name) { export function isNative (name) {
const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons', 'solidity', 'hardhat-provider'] const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons', 'solidity', 'hardhat-provider', 'solidityStaticAnalysis', 'solidityUnitTesting']
return nativePlugins.includes(name) || requiredModules.includes(name) return nativePlugins.includes(name) || requiredModules.includes(name)
} }

@ -13,6 +13,7 @@ export class RemixEngine extends Engine {
if (name === 'dGitProvider') return { queueTimeout: 60000 * 4 } if (name === 'dGitProvider') return { queueTimeout: 60000 * 4 }
if (name === 'slither') return { queueTimeout: 60000 * 4 } // Requires when a solc version is installed if (name === 'slither') return { queueTimeout: 60000 * 4 } // Requires when a solc version is installed
if (name === 'hardhat') return { queueTimeout: 60000 * 4 } if (name === 'hardhat') return { queueTimeout: 60000 * 4 }
if (name === 'localPlugin') return { queueTimeout: 60000 * 4 }
return { queueTimeout: 10000 } return { queueTimeout: 10000 }
} }

@ -30,7 +30,6 @@ const defaultCompilerParameters = {
evmVersion: null, // compiler default evmVersion: null, // compiler default
language: 'Solidity' language: 'Solidity'
} }
export class CompilerClientApi extends CompilerApiMixin(PluginClient) implements ICompilerApi { export class CompilerClientApi extends CompilerApiMixin(PluginClient) implements ICompilerApi {
constructor () { constructor () {
super() super()

@ -4,7 +4,8 @@
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off",
"no-unused-vars": "off", "no-unused-vars": "off",
"dot-notation": "off" "dot-notation": "off",
"no-use-before-define": "off"
}, },
"ignorePatterns": ["!**/*"] "ignorePatterns": ["!**/*"]
} }

@ -1,6 +1,6 @@
{ {
"name": "@remix-project/remix-analyzer", "name": "@remix-project/remix-analyzer",
"version": "0.5.16", "version": "0.5.17",
"description": "Tool to perform static analysis on Solidity smart contracts", "description": "Tool to perform static analysis on Solidity smart contracts",
"main": "src/index.js", "main": "src/index.js",
"types": "src/index.d.ts", "types": "src/index.d.ts",
@ -19,11 +19,11 @@
} }
], ],
"dependencies": { "dependencies": {
"@ethereumjs/block": "^3.4.0", "@ethereumjs/block": "^3.5.1",
"@ethereumjs/tx": "^3.3.0", "@ethereumjs/tx": "^3.3.2",
"@ethereumjs/vm": "^5.5.0", "@ethereumjs/vm": "^5.5.3",
"@remix-project/remix-astwalker": "^0.0.37", "@remix-project/remix-astwalker": "^0.0.38",
"@remix-project/remix-lib": "^0.5.7", "@remix-project/remix-lib": "^0.5.8",
"async": "^2.6.2", "async": "^2.6.2",
"ethereumjs-util": "^7.0.10", "ethereumjs-util": "^7.0.10",
"ethers": "^5.4.2", "ethers": "^5.4.2",
@ -51,5 +51,5 @@
"typescript": "^3.7.5" "typescript": "^3.7.5"
}, },
"typings": "src/index.d.ts", "typings": "src/index.d.ts",
"gitHead": "4705cbc4d1761f75267992552da9db6cff2f3ed5" "gitHead": "13f961dadacea374de4d79a70b1aa4acf243b18b"
} }

@ -3,7 +3,8 @@
"rules": { "rules": {
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/prefer-namespace-keyword": "off", "@typescript-eslint/prefer-namespace-keyword": "off",
"no-unused-vars": "off" "no-unused-vars": "off",
"no-use-before-define": "off"
}, },
"ignorePatterns": ["!**/*"] "ignorePatterns": ["!**/*"]
} }

@ -1,6 +1,6 @@
{ {
"name": "@remix-project/remix-astwalker", "name": "@remix-project/remix-astwalker",
"version": "0.0.37", "version": "0.0.38",
"description": "Tool to walk through Solidity AST", "description": "Tool to walk through Solidity AST",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
@ -34,10 +34,10 @@
] ]
}, },
"dependencies": { "dependencies": {
"@ethereumjs/block": "^3.4.0", "@ethereumjs/block": "^3.5.1",
"@ethereumjs/tx": "^3.3.0", "@ethereumjs/tx": "^3.3.2",
"@ethereumjs/vm": "^5.5.0", "@ethereumjs/vm": "^5.5.3",
"@remix-project/remix-lib": "^0.5.7", "@remix-project/remix-lib": "^0.5.8",
"@types/tape": "^4.2.33", "@types/tape": "^4.2.33",
"async": "^2.6.2", "async": "^2.6.2",
"ethereumjs-util": "^7.0.10", "ethereumjs-util": "^7.0.10",
@ -53,5 +53,5 @@
"tap-spec": "^5.0.0" "tap-spec": "^5.0.0"
}, },
"typings": "src/index.d.ts", "typings": "src/index.d.ts",
"gitHead": "4705cbc4d1761f75267992552da9db6cff2f3ed5" "gitHead": "13f961dadacea374de4d79a70b1aa4acf243b18b"
} }

@ -39,6 +39,7 @@ export function isYulAstNode (node: Record<string, unknown>): boolean {
* in each case, if the event emits false it does not descend into children. * in each case, if the event emits false it does not descend into children.
* If no event for the current type, children are visited. * If no event for the current type, children are visited.
*/ */
// eslint-disable-next-line no-redeclare
export class AstWalker extends EventEmitter { export class AstWalker extends EventEmitter {
manageCallback ( manageCallback (
node: AstNode, node: AstNode,

@ -58,6 +58,7 @@ export function sourceLocationFromSrc (src: string): Location {
* Routines for retrieving solc AST object(s) using some criteria, usually * Routines for retrieving solc AST object(s) using some criteria, usually
* includng "src' information. * includng "src' information.
*/ */
// eslint-disable-next-line no-redeclare
export class SourceMappings { export class SourceMappings {
readonly source: string; readonly source: string;
readonly lineBreaks: Array<number>; readonly lineBreaks: Array<number>;

@ -4,7 +4,7 @@ import { CompilerAbstract } from '@remix-project/remix-solidity'
const profile = { const profile = {
name: 'compilerArtefacts', name: 'compilerArtefacts',
methods: ['get', 'addResolvedContract'], methods: ['get', 'addResolvedContract', 'getCompilerAbstract'],
events: [], events: [],
version: '0.0.1' version: '0.0.1'
} }

@ -1,6 +1,6 @@
{ {
"name": "@remix-project/remix-debug", "name": "@remix-project/remix-debug",
"version": "0.5.7", "version": "0.5.8",
"description": "Tool to debug Ethereum transactions", "description": "Tool to debug Ethereum transactions",
"contributors": [ "contributors": [
{ {
@ -18,19 +18,24 @@
], ],
"main": "src/index.js", "main": "src/index.js",
"dependencies": { "dependencies": {
"@ethereumjs/block": "^3.4.0", "@ethereumjs/block": "^3.5.1",
"@ethereumjs/common": "^2.2.0", "@ethereumjs/common": "^2.5.0",
"@ethereumjs/tx": "^3.3.0", "@ethereumjs/tx": "^3.3.2",
"@ethereumjs/vm": "^5.5.0", "@ethereumjs/vm": "^5.5.3",
"@remix-project/remix-astwalker": "^0.0.37", "@remix-project/remix-astwalker": "^0.0.38",
"@remix-project/remix-lib": "^0.5.7", "@remix-project/remix-lib": "^0.5.8",
"@remix-project/remix-simulator": "^0.2.7", "@remix-project/remix-simulator": "^0.2.8",
"ansi-gray": "^0.1.1",
"async": "^2.6.2", "async": "^2.6.2",
"color-support": "^1.1.3",
"commander": "^2.19.0", "commander": "^2.19.0",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"ethereumjs-util": "^7.0.10", "ethereumjs-util": "^7.0.10",
"ethers": "^5.4.2", "ethers": "^5.4.2",
"express-ws": "^4.0.0",
"merge": "^2.1.1",
"string-similarity": "^4.0.4", "string-similarity": "^4.0.4",
"time-stamp": "^2.2.0",
"web3": "^1.5.1" "web3": "^1.5.1"
}, },
"devDependencies": { "devDependencies": {
@ -62,5 +67,5 @@
}, },
"homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-debug#readme", "homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-debug#readme",
"typings": "src/index.d.ts", "typings": "src/index.d.ts",
"gitHead": "4705cbc4d1761f75267992552da9db6cff2f3ed5" "gitHead": "13f961dadacea374de4d79a70b1aa4acf243b18b"
} }

@ -3,7 +3,8 @@
"rules": { "rules": {
"standard/no-callback-literal": "off", "standard/no-callback-literal": "off",
"no-unused-vars": "off", "no-unused-vars": "off",
"dot-notation": "off" "dot-notation": "off",
"no-use-before-define": "off"
}, },
"env": { "env": {
"browser": true, "browser": true,

@ -1,6 +1,6 @@
{ {
"name": "@remix-project/remix-lib", "name": "@remix-project/remix-lib",
"version": "0.5.7", "version": "0.5.8",
"description": "Library to various Remix tools", "description": "Library to various Remix tools",
"contributors": [ "contributors": [
{ {
@ -14,12 +14,13 @@
], ],
"main": "src/index.js", "main": "src/index.js",
"dependencies": { "dependencies": {
"@ethereumjs/block": "^3.4.0", "@ethereumjs/block": "^3.5.1",
"@ethereumjs/tx": "^3.3.0", "@ethereumjs/tx": "^3.3.2",
"@ethereumjs/vm": "^5.5.0", "@ethereumjs/vm": "^5.5.3",
"async": "^2.1.2", "async": "^2.1.2",
"ethereumjs-util": "^7.0.10", "ethereumjs-util": "^7.0.10",
"ethers": "^4.0.40", "ethers": "^4.0.40",
"ethjs-util": "^0.1.6",
"events": "^3.0.0", "events": "^3.0.0",
"solc": "^0.7.4", "solc": "^0.7.4",
"string-similarity": "^4.0.4", "string-similarity": "^4.0.4",
@ -53,5 +54,5 @@
}, },
"homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-lib#readme", "homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-lib#readme",
"typings": "src/index.d.ts", "typings": "src/index.d.ts",
"gitHead": "4705cbc4d1761f75267992552da9db6cff2f3ed5" "gitHead": "13f961dadacea374de4d79a70b1aa4acf243b18b"
} }

@ -129,11 +129,7 @@ export class Web3VmProvider {
this.storageCache[this.processingHash] = {} this.storageCache[this.processingHash] = {}
if (data.to) { if (data.to) {
try { try {
// dumpStorage throws error as 'Missing Node in DB' const storage = await this.vm.stateManager.dumpStorage(data.to)
// This can be uncommented once that error is handled
// https://github.com/ethereum/remix-project/issues/1644
// const storage = await this.vm.stateManager.dumpStorage(data.to)
const storage = {}
this.storageCache[this.processingHash][tx['to']] = storage this.storageCache[this.processingHash][tx['to']] = storage
this.lastProcessedStorageTxHash[tx['to']] = this.processingHash this.lastProcessedStorageTxHash[tx['to']] = this.processingHash
} catch (e) { } catch (e) {
@ -249,11 +245,7 @@ export class Web3VmProvider {
if (!this.storageCache[this.processingHash][this.processingAddress]) { if (!this.storageCache[this.processingHash][this.processingAddress]) {
const account = Address.fromString(this.processingAddress) const account = Address.fromString(this.processingAddress)
try { try {
// dumpStorage throws error as 'Missing Node in DB' const storage = await this.vm.stateManager.dumpStorage(account)
// This can be uncommented once that error is handled
// https://github.com/ethereum/remix-project/issues/1644
// const storage = await this.vm.stateManager.dumpStorage(account)
const storage = {}
this.storageCache[this.processingHash][this.processingAddress] = storage this.storageCache[this.processingHash][this.processingAddress] = storage
this.lastProcessedStorageTxHash[this.processingAddress] = this.processingHash this.lastProcessedStorageTxHash[this.processingAddress] = this.processingHash
} catch (e) { } catch (e) {

@ -1,6 +1,6 @@
{ {
"name": "@remix-project/remix-simulator", "name": "@remix-project/remix-simulator",
"version": "0.2.7", "version": "0.2.8",
"description": "Ethereum IDE and tools for the web", "description": "Ethereum IDE and tools for the web",
"contributors": [ "contributors": [
{ {
@ -14,11 +14,11 @@
], ],
"main": "src/index.js", "main": "src/index.js",
"dependencies": { "dependencies": {
"@ethereumjs/block": "^3.4.0", "@ethereumjs/block": "^3.5.1",
"@ethereumjs/common": "^2.2.0", "@ethereumjs/common": "^2.5.0",
"@ethereumjs/tx": "^3.3.0", "@ethereumjs/tx": "^3.3.2",
"@ethereumjs/vm": "^5.5.0", "@ethereumjs/vm": "^5.5.3",
"@remix-project/remix-lib": "^0.5.7", "@remix-project/remix-lib": "^0.5.8",
"ansi-gray": "^0.1.1", "ansi-gray": "^0.1.1",
"async": "^3.1.0", "async": "^3.1.0",
"body-parser": "^1.18.2", "body-parser": "^1.18.2",
@ -66,5 +66,5 @@
}, },
"homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-simulator#readme", "homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-simulator#readme",
"typings": "src/index.d.ts", "typings": "src/index.d.ts",
"gitHead": "4705cbc4d1761f75267992552da9db6cff2f3ed5" "gitHead": "13f961dadacea374de4d79a70b1aa4acf243b18b"
} }

@ -115,8 +115,6 @@ describe('blocks', () => {
assert.deepEqual(numberTransactions, correctBlock.uncles.length) assert.deepEqual(numberTransactions, correctBlock.uncles.length)
}) })
}) })
/*
describe('eth_getStorageAt', () => { describe('eth_getStorageAt', () => {
it('should get storage at position at given address', async () => { it('should get storage at position at given address', async () => {
const abi: any = [ const abi: any = [
@ -201,30 +199,28 @@ describe('blocks', () => {
} }
] ]
// const code = '0x608060405234801561001057600080fd5b506040516020806102018339810180604052602081101561003057600080fd5b810190808051906020019092919050505080600081905550506101a9806100586000396000f3fe60806040526004361061005c576000357c0100000000000000000000000000000000000000000000000000000000900480632a1afcd91461006157806360fe47b11461008c5780636d4ce63c146100c7578063ce01e1ec146100f2575b600080fd5b34801561006d57600080fd5b5061007661012d565b6040518082815260200191505060405180910390f35b34801561009857600080fd5b506100c5600480360360208110156100af57600080fd5b8101908080359060200190929190505050610133565b005b3480156100d357600080fd5b506100dc61013d565b6040518082815260200191505060405180910390f35b3480156100fe57600080fd5b5061012b6004803603602081101561011557600080fd5b8101908080359060200190929190505050610146565b005b60005481565b8060008190555050565b60008054905090565b80600081905550807f63a242a632efe33c0e210e04e4173612a17efa4f16aa4890bc7e46caece80de060405160405180910390a25056fea165627a7a7230582063160eb16dc361092a85ced1a773eed0b63738b83bea1e1c51cf066fa90e135d0029' const code = '0x608060405234801561001057600080fd5b506040516020806102018339810180604052602081101561003057600080fd5b810190808051906020019092919050505080600081905550506101a9806100586000396000f3fe60806040526004361061005c576000357c0100000000000000000000000000000000000000000000000000000000900480632a1afcd91461006157806360fe47b11461008c5780636d4ce63c146100c7578063ce01e1ec146100f2575b600080fd5b34801561006d57600080fd5b5061007661012d565b6040518082815260200191505060405180910390f35b34801561009857600080fd5b506100c5600480360360208110156100af57600080fd5b8101908080359060200190929190505050610133565b005b3480156100d357600080fd5b506100dc61013d565b6040518082815260200191505060405180910390f35b3480156100fe57600080fd5b5061012b6004803603602081101561011557600080fd5b8101908080359060200190929190505050610146565b005b60005481565b8060008190555050565b60008054905090565b80600081905550807f63a242a632efe33c0e210e04e4173612a17efa4f16aa4890bc7e46caece80de060405160405180910390a25056fea165627a7a7230582063160eb16dc361092a85ced1a773eed0b63738b83bea1e1c51cf066fa90e135d0029'
// const contract = new web3.eth.Contract(abi) const contract = new web3.eth.Contract(abi)
// const accounts = await web3.eth.getAccounts() const accounts = await web3.eth.getAccounts()
// const contractInstance: any = await contract.deploy({ data: code, arguments: [100] }).send({ from: accounts[0], gas: 400000 }) const contractInstance: any = await contract.deploy({ data: code, arguments: [100] }).send({ from: accounts[0], gas: 400000 })
// contractInstance.currentProvider = web3.eth.currentProvider contractInstance.currentProvider = web3.eth.currentProvider
// contractInstance.givenProvider = web3.eth.currentProvider contractInstance.givenProvider = web3.eth.currentProvider
// await contractInstance.methods.set(100).send({ from: accounts[0].toLowerCase(), gas: 400000 }) await contractInstance.methods.set(100).send({ from: accounts[0].toLowerCase(), gas: 400000 })
// let storage = await web3.eth.getStorageAt(contractInstance.options.address, 0) let storage = await web3.eth.getStorageAt(contractInstance.options.address, 0)
// assert.deepEqual(storage, '0x64') assert.deepEqual(storage, '0x64')
// await contractInstance.methods.set(200).send({ from: accounts[0], gas: 400000 }) await contractInstance.methods.set(200).send({ from: accounts[0], gas: 400000 })
// storage = await web3.eth.getStorageAt(contractInstance.options.address, 0) storage = await web3.eth.getStorageAt(contractInstance.options.address, 0)
// assert.deepEqual(storage, '0x64') assert.deepEqual(storage, '0x64')
await contractInstance.methods.set(200).send({ from: accounts[0], gas: 400000 }) await contractInstance.methods.set(200).send({ from: accounts[0], gas: 400000 })
storage = await web3.eth.getStorageAt(contractInstance.options.address, 0) storage = await web3.eth.getStorageAt(contractInstance.options.address, 0)
assert.deepEqual(storage, '0xc8') assert.deepEqual(storage, '0xc8')
}) })
}) })
*/
describe('eth_call', () => { describe('eth_call', () => {
it('should get a value', async () => { it('should get a value', async () => {
const abi: any = [ const abi: any = [

@ -2,7 +2,8 @@
"extends": "../../.eslintrc", "extends": "../../.eslintrc",
"rules": { "rules": {
"dot-notation": "off", "dot-notation": "off",
"no-unused-vars": "off" "no-unused-vars": "off",
"no-use-before-define": "off"
}, },
"env": { "env": {
"browser": true, "browser": true,

@ -1,6 +1,6 @@
{ {
"name": "@remix-project/remix-solidity", "name": "@remix-project/remix-solidity",
"version": "0.4.7", "version": "0.4.8",
"description": "Tool to load and run Solidity compiler", "description": "Tool to load and run Solidity compiler",
"main": "src/index.js", "main": "src/index.js",
"types": "src/index.d.ts", "types": "src/index.d.ts",
@ -15,10 +15,10 @@
} }
], ],
"dependencies": { "dependencies": {
"@ethereumjs/block": "^3.4.0", "@ethereumjs/block": "^3.5.1",
"@ethereumjs/tx": "^3.3.0", "@ethereumjs/tx": "^3.3.2",
"@ethereumjs/vm": "^5.5.0", "@ethereumjs/vm": "^5.5.3",
"@remix-project/remix-lib": "^0.5.7", "@remix-project/remix-lib": "^0.5.8",
"async": "^2.6.2", "async": "^2.6.2",
"eslint-scope": "^5.0.0", "eslint-scope": "^5.0.0",
"ethereumjs-util": "^7.0.10", "ethereumjs-util": "^7.0.10",
@ -61,5 +61,5 @@
}, },
"homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-solidity#readme", "homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-solidity#readme",
"typings": "src/index.d.ts", "typings": "src/index.d.ts",
"gitHead": "4705cbc4d1761f75267992552da9db6cff2f3ed5" "gitHead": "13f961dadacea374de4d79a70b1aa4acf243b18b"
} }

@ -150,7 +150,7 @@ export interface CompilerInputOptions {
language?: Language language?: Language
} }
export type EVMVersion = 'homestead' | 'tangerineWhistle' | 'spuriousDragon' | 'byzantium' | 'constantinople' | 'petersburg' | 'istanbul' | 'muirGlacier' | 'berlin' | 'london' | null export type EVMVersion = 'homestead' | 'tangerineWhistle' | 'spuriousDragon' | 'byzantium' | 'constantinople' | 'petersburg' | 'istanbul' | 'berlin' | 'london' | null
export type Language = 'Solidity' | 'Yul' export type Language = 'Solidity' | 'Yul'

@ -2,7 +2,8 @@
"extends": "../../.eslintrc", "extends": "../../.eslintrc",
"rules": { "rules": {
"dot-notation": "off", "dot-notation": "off",
"no-unused-vars": "off" "no-unused-vars": "off",
"no-use-before-define": "off"
}, },
"env": { "env": {
"browser": true, "browser": true,

@ -6,7 +6,7 @@ module.exports = {
transform: { transform: {
'^.+\\.[tj]sx?$': 'ts-jest', '^.+\\.[tj]sx?$': 'ts-jest',
}, },
transformIgnorePatterns: ["/node_modules/", "\\.pnp\\.[^\\\/]+$"], transformIgnorePatterns: ["/node_modules/", "/dist/", "\\.pnp\\.[^\\\/]+$"],
rootDir: "./", rootDir: "./",
testTimeout: 40000, testTimeout: 40000,
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html', 'json'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html', 'json'],

@ -1,6 +1,6 @@
{ {
"name": "@remix-project/remix-tests", "name": "@remix-project/remix-tests",
"version": "0.2.7", "version": "0.2.8",
"description": "Tool to test Solidity smart contracts", "description": "Tool to test Solidity smart contracts",
"main": "src/index.js", "main": "src/index.js",
"types": "./src/index.d.ts", "types": "./src/index.d.ts",
@ -35,13 +35,13 @@
}, },
"homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-tests#readme", "homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-tests#readme",
"dependencies": { "dependencies": {
"@ethereumjs/block": "^3.4.0", "@ethereumjs/block": "^3.5.1",
"@ethereumjs/common": "^2.2.0", "@ethereumjs/common": "^2.5.0",
"@ethereumjs/tx": "^3.3.0", "@ethereumjs/tx": "^3.3.2",
"@ethereumjs/vm": "^5.5.0", "@ethereumjs/vm": "^5.5.3",
"@remix-project/remix-lib": "^0.5.7", "@remix-project/remix-lib": "^0.5.8",
"@remix-project/remix-simulator": "^0.2.7", "@remix-project/remix-simulator": "^0.2.8",
"@remix-project/remix-solidity": "^0.4.7", "@remix-project/remix-solidity": "^0.4.8",
"ansi-gray": "^0.1.1", "ansi-gray": "^0.1.1",
"async": "^2.6.0", "async": "^2.6.0",
"axios": ">=0.21.1", "axios": ">=0.21.1",
@ -77,5 +77,5 @@
"typescript": "^3.3.1" "typescript": "^3.3.1"
}, },
"typings": "src/index.d.ts", "typings": "src/index.d.ts",
"gitHead": "4705cbc4d1761f75267992552da9db6cff2f3ed5" "gitHead": "13f961dadacea374de4d79a70b1aa4acf243b18b"
} }

@ -171,8 +171,9 @@ export function compileFileOrFiles (filename: string, isDirectory: boolean, opts
* @param cb Callback * @param cb Callback
*/ */
export function compileContractSources (sources: SrcIfc, compilerConfig: CompilerConfiguration, importFileCb: any, opts: any, cb): void { export function compileContractSources (sources: SrcIfc, compilerConfig: CompilerConfiguration, importFileCb: any, opts: any, cb): void {
let compiler, filepath: string let compiler
const accounts: string[] = opts.accounts || [] const accounts: string[] = opts.accounts || []
const filepath = opts.testFilePath || ''
// Iterate over sources keys. Inject test libraries. Inject test library import statements. // Iterate over sources keys. Inject test libraries. Inject test library import statements.
if (!('remix_tests.sol' in sources) && !('tests.sol' in sources)) { if (!('remix_tests.sol' in sources) && !('tests.sol' in sources)) {
sources['tests.sol'] = { content: require('../sol/tests.sol.js') } sources['tests.sol'] = { content: require('../sol/tests.sol.js') }

@ -11,7 +11,7 @@ import { compilationInterface } from './types'
* @param callback Callback * @param callback Callback
*/ */
export function deployAll (compileResult: compilationInterface, web3: Web3, withDoubleGas: boolean, callback) { export function deployAll (compileResult: compilationInterface, web3: Web3, withDoubleGas: boolean, deployCb, callback) {
const compiledObject = {} const compiledObject = {}
const contracts = {} const contracts = {}
let accounts: string[] = [] let accounts: string[] = []
@ -70,7 +70,7 @@ export function deployAll (compileResult: compilationInterface, web3: Web3, with
deployObject.send({ deployObject.send({
from: accounts[0], from: accounts[0],
gas: gas gas: gas
}).on('receipt', function (receipt) { }).on('receipt', async function (receipt) {
contractObject.options.address = receipt.contractAddress contractObject.options.address = receipt.contractAddress
contractObject.options.from = accounts[0] contractObject.options.from = accounts[0]
contractObject.options.gas = 5000 * 1000 contractObject.options.gas = 5000 * 1000
@ -79,6 +79,7 @@ export function deployAll (compileResult: compilationInterface, web3: Web3, with
contracts[contractName] = contractObject contracts[contractName] = contractObject
contracts[contractName].filename = filename contracts[contractName].filename = filename
if (deployCb) await deployCb(filename, receipt.contractAddress)
callback(null, { receipt: { contractAddress: receipt.contractAddress } }) // TODO this will only work with JavaScriptV VM callback(null, { receipt: { contractAddress: receipt.contractAddress } }) // TODO this will only work with JavaScriptV VM
}).on('error', function (err) { }).on('error', function (err) {
console.error(err) console.error(err)

@ -81,8 +81,8 @@ commander
const compVersion = commander.compiler const compVersion = commander.compiler
const baseURL = 'https://binaries.soliditylang.org/wasm/' const baseURL = 'https://binaries.soliditylang.org/wasm/'
const response: AxiosResponse = await axios.get(baseURL + 'list.json') const response: AxiosResponse = await axios.get(baseURL + 'list.json')
const { releases, latestRelease } = response.data const { releases, latestRelease } = response.data as { releases: string[], latestRelease: string }
const compString = releases[compVersion] const compString = releases ? releases[compVersion] : null
if (!compString) { if (!compString) {
log.error(`No compiler found in releases with version ${compVersion}`) log.error(`No compiler found in releases with version ${compVersion}`)
process.exit() process.exit()

@ -61,13 +61,13 @@ export function runTestFiles (filepath: string, isDirectory: boolean, web3: Web3
for (const filename in asts) { for (const filename in asts) {
if (filename.endsWith('_test.sol')) { sourceASTs[filename] = asts[filename].ast } if (filename.endsWith('_test.sol')) { sourceASTs[filename] = asts[filename].ast }
} }
deployAll(compilationResult, web3, false, (err, contracts) => { deployAll(compilationResult, web3, false, null, (err, contracts) => {
if (err) { if (err) {
// If contract deployment fails because of 'Out of Gas' error, try again with double gas // If contract deployment fails because of 'Out of Gas' error, try again with double gas
// This is temporary, should be removed when remix-tests will have a dedicated UI to // This is temporary, should be removed when remix-tests will have a dedicated UI to
// accept deployment params from UI // accept deployment params from UI
if (err.message.includes('The contract code couldn\'t be stored, please check your gas limit')) { if (err.message.includes('The contract code couldn\'t be stored, please check your gas limit')) {
deployAll(compilationResult, web3, true, (error, contracts) => { deployAll(compilationResult, web3, true, null, (error, contracts) => {
if (error) next([{ message: 'contract deployment failed after trying twice: ' + error.message, severity: 'error' }]) // IDE expects errors in array if (error) next([{ message: 'contract deployment failed after trying twice: ' + error.message, severity: 'error' }]) // IDE expects errors in array
else next(null, compilationResult, contracts) else next(null, compilationResult, contracts)
}) })

@ -39,7 +39,7 @@ export class UnitTestRunner {
* @param importFileCb Import file callback * @param importFileCb Import file callback
* @param opts Options * @param opts Options
*/ */
async runTestSources (contractSources: SrcIfc, compilerConfig: CompilerConfiguration, testCallback, resultCallback, finalCallback: any, importFileCb, opts: Options) { async runTestSources (contractSources: SrcIfc, compilerConfig: CompilerConfiguration, testCallback, resultCallback, deployCb:any, finalCallback: any, importFileCb, opts: Options) {
opts = opts || {} opts = opts || {}
const sourceASTs: any = {} const sourceASTs: any = {}
const web3 = opts.web3 || await this.createWeb3Provider() const web3 = opts.web3 || await this.createWeb3Provider()
@ -53,19 +53,19 @@ export class UnitTestRunner {
}) })
}, },
(next) => { (next) => {
compileContractSources(contractSources, compilerConfig, importFileCb, { accounts, event: this.event }, next) compileContractSources(contractSources, compilerConfig, importFileCb, { accounts, testFilePath: opts.testFilePath, event: this.event }, next)
}, },
function deployAllContracts (compilationResult: compilationInterface, asts: ASTInterface, next) { function deployAllContracts (compilationResult: compilationInterface, asts: ASTInterface, next) {
for (const filename in asts) { for (const filename in asts) {
if (filename.endsWith('_test.sol')) { sourceASTs[filename] = asts[filename].ast } if (filename.endsWith('_test.sol')) { sourceASTs[filename] = asts[filename].ast }
} }
deployAll(compilationResult, web3, false, (err, contracts) => { deployAll(compilationResult, web3, false, deployCb, (err, contracts) => {
if (err) { if (err) {
// If contract deployment fails because of 'Out of Gas' error, try again with double gas // If contract deployment fails because of 'Out of Gas' error, try again with double gas
// This is temporary, should be removed when remix-tests will have a dedicated UI to // This is temporary, should be removed when remix-tests will have a dedicated UI to
// accept deployment params from UI // accept deployment params from UI
if (err.message.includes('The contract code couldn\'t be stored, please check your gas limit')) { if (err.message.includes('The contract code couldn\'t be stored, please check your gas limit')) {
deployAll(compilationResult, web3, true, (error, contracts) => { deployAll(compilationResult, web3, true, deployCb, (error, contracts) => {
if (error) next([{ message: 'contract deployment failed after trying twice: ' + error.message, severity: 'error' }]) // IDE expects errors in array if (error) next([{ message: 'contract deployment failed after trying twice: ' + error.message, severity: 'error' }]) // IDE expects errors in array
else next(null, compilationResult, contracts) else next(null, compilationResult, contracts)
}) })

@ -244,6 +244,7 @@ export function runTest (testName: string, testObject: any, contractDetails: Com
if (func.inputs && func.inputs.length > 0) { return resultsCallback(new Error(`Method '${func.name}' can not have parameters inside a test contract`), { passingNum, failureNum, timePassed }) } if (func.inputs && func.inputs.length > 0) { return resultsCallback(new Error(`Method '${func.name}' can not have parameters inside a test contract`), { passingNum, failureNum, timePassed }) }
const method = testObject.methods[func.name].apply(testObject.methods[func.name], []) const method = testObject.methods[func.name].apply(testObject.methods[func.name], [])
const startTime = Date.now() const startTime = Date.now()
let debugTxHash:string
if (func.constant) { if (func.constant) {
sendParams = {} sendParams = {}
const tagTimestamp = 'remix_tests_tag' + Date.now() const tagTimestamp = 'remix_tests_tag' + Date.now()
@ -251,18 +252,20 @@ export function runTest (testName: string, testObject: any, contractDetails: Com
method.call(sendParams).then(async (result) => { method.call(sendParams).then(async (result) => {
const time = (Date.now() - startTime) / 1000.0 const time = (Date.now() - startTime) / 1000.0
let tagTxHash let tagTxHash
let hhLogs
if (web3.eth && web3.eth.getHashFromTagBySimulator) tagTxHash = await web3.eth.getHashFromTagBySimulator(tagTimestamp) if (web3.eth && web3.eth.getHashFromTagBySimulator) tagTxHash = await web3.eth.getHashFromTagBySimulator(tagTimestamp)
if (web3.eth && web3.eth.getHHLogsForTx) hhLogs = await web3.eth.getHHLogsForTx(tagTxHash) if (web3.eth && web3.eth.getHHLogsForTx) hhLogs = await web3.eth.getHHLogsForTx(tagTxHash)
debugTxHash = tagTxHash
if (result) { if (result) {
const resp: TestResultInterface = { const resp: TestResultInterface = {
type: 'testPass', type: 'testPass',
value: changeCase.sentenceCase(func.name), value: changeCase.sentenceCase(func.name),
filename: testObject.filename, filename: testObject.filename,
time: time, time: time,
context: testName context: testName,
web3,
debugTxHash
} }
if (hhLogs) resp.hhLogs = hhLogs if (hhLogs && hhLogs.length) resp.hhLogs = hhLogs
testCallback(undefined, resp) testCallback(undefined, resp)
passingNum += 1 passingNum += 1
timePassed += time timePassed += time
@ -273,9 +276,11 @@ export function runTest (testName: string, testObject: any, contractDetails: Com
filename: testObject.filename, filename: testObject.filename,
time: time, time: time,
errMsg: 'function returned false', errMsg: 'function returned false',
context: testName context: testName,
web3,
debugTxHash
} }
if (hhLogs) resp.hhLogs = hhLogs if (hhLogs && hhLogs.length) resp.hhLogs = hhLogs
testCallback(undefined, resp) testCallback(undefined, resp)
failureNum += 1 failureNum += 1
timePassed += time timePassed += time
@ -294,6 +299,7 @@ export function runTest (testName: string, testObject: any, contractDetails: Com
sendParams.gas = 10000000 * 8 sendParams.gas = 10000000 * 8
method.send(sendParams).on('receipt', async (receipt) => { method.send(sendParams).on('receipt', async (receipt) => {
try { try {
debugTxHash = receipt.transactionHash
if (web3.eth && web3.eth.getHHLogsForTx) hhLogs = await web3.eth.getHHLogsForTx(receipt.transactionHash) if (web3.eth && web3.eth.getHHLogsForTx) hhLogs = await web3.eth.getHHLogsForTx(receipt.transactionHash)
const time: number = (Date.now() - startTime) / 1000.0 const time: number = (Date.now() - startTime) / 1000.0
const assertionEventHashes = assertionEvents.map(e => Web3.utils.sha3(e.name + '(' + e.params.join() + ')')) const assertionEventHashes = assertionEvents.map(e => Web3.utils.sha3(e.name + '(' + e.params.join() + ')'))
@ -323,9 +329,10 @@ export function runTest (testName: string, testObject: any, contractDetails: Com
returned: testEvent[3], returned: testEvent[3],
expected: testEvent[4], expected: testEvent[4],
location, location,
web3 web3,
debugTxHash
} }
if (hhLogs) resp.hhLogs = hhLogs if (hhLogs && hhLogs.length) resp.hhLogs = hhLogs
testCallback(undefined, resp) testCallback(undefined, resp)
failureNum += 1 failureNum += 1
timePassed += time timePassed += time
@ -342,13 +349,15 @@ export function runTest (testName: string, testObject: any, contractDetails: Com
value: changeCase.sentenceCase(func.name), value: changeCase.sentenceCase(func.name),
filename: testObject.filename, filename: testObject.filename,
time: time, time: time,
context: testName context: testName,
web3,
debugTxHash
} }
if (hhLogs) resp.hhLogs = hhLogs if (hhLogs && hhLogs.length) resp.hhLogs = hhLogs
testCallback(undefined, resp) testCallback(undefined, resp)
passingNum += 1 passingNum += 1
timePassed += time timePassed += time
} else if (hhLogs) { } else if (hhLogs && hhLogs.length) {
const resp: TestResultInterface = { const resp: TestResultInterface = {
type: 'logOnly', type: 'logOnly',
value: changeCase.sentenceCase(func.name), value: changeCase.sentenceCase(func.name),
@ -380,7 +389,8 @@ export function runTest (testName: string, testObject: any, contractDetails: Com
if (err.message.includes('Transaction has been reverted by the EVM')) { if (err.message.includes('Transaction has been reverted by the EVM')) {
const txHash = JSON.parse(err.message.replace('Transaction has been reverted by the EVM:', '')).transactionHash const txHash = JSON.parse(err.message.replace('Transaction has been reverted by the EVM:', '')).transactionHash
if (web3.eth && web3.eth.getHHLogsForTx) hhLogs = await web3.eth.getHHLogsForTx(txHash) if (web3.eth && web3.eth.getHHLogsForTx) hhLogs = await web3.eth.getHHLogsForTx(txHash)
if (hhLogs) resp.hhLogs = hhLogs if (hhLogs && hhLogs.length) resp.hhLogs = hhLogs
resp.debugTxHash = txHash
} }
testCallback(undefined, resp) testCallback(undefined, resp)
failureNum += 1 failureNum += 1

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

Loading…
Cancel
Save