diff --git a/apps/remix-ide-e2e/src/tests/stress.editor.ts b/apps/remix-ide-e2e/src/tests/stress.editor.ts new file mode 100644 index 0000000000..ddb007b12a --- /dev/null +++ b/apps/remix-ide-e2e/src/tests/stress.editor.ts @@ -0,0 +1,195 @@ +'use strict' + +import { NightwatchBrowser } from 'nightwatch' +import init from '../helpers/init' + +module.exports = { + + before: function (browser: NightwatchBrowser, done: VoidFunction) { + init(browser, done) + }, + + 'Should create 10 files, reload, and check if the files are saved': function (browser: NightwatchBrowser) { + const contents = {} + const checkContent = function (i, done) { + const name = 'test_' + i + '.sol' + browser + .openFile(name) + .pause(500) + .getEditorValue((content) => { + browser.assert.ok(content === contents[i]) + done() + }) + } + browser.clickLaunchIcon('filePanel').perform((done) => { + let contentEditSet = content.slice() + for (let i = 0; i < 10; i++) { + contentEditSet += contentEditSet + contents[i] = contentEditSet + const name = 'test_' + i + '.sol' + browser.click('[data-id="fileExplorerNewFilecreateNewFile"]') + .waitForElementContainsText('*[data-id$="/blank"]', '', 60000) + .sendKeys('*[data-id$="/blank"] .remixui_items', name) + .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER) + .waitForElementVisible(`li[data-id="treeViewLitreeViewItem${name}"]`, 60000) + .setEditorValue(contentEditSet) + } + done() + }).pause(10000).refresh() + .perform(done => checkContent(0, done)) + .perform(done => checkContent(1, done)) + .perform(done => checkContent(2, done)) + .perform(done => checkContent(3, done)) + .perform(done => checkContent(4, done)) + .perform(done => checkContent(5, done)) + .perform(done => checkContent(6, done)) + .perform(done => checkContent(7, done)) + .perform(done => checkContent(8, done)) + .perform(done => checkContent(9, done)) + .end() + } +} + +const content = ` +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.7.0 <0.9.0; + +/** + * @title Ballot + * @dev Implements voting process along with vote delegation| + */ +contract Ballot { + + struct Voter { + uint weight; // weight is accumulated by delegation + bool voted; // if true, that person already voted + address delegate; // person delegated to + uint vote; // index of the voted proposal + } + + struct Proposal { + // If you can limit the length to a certain number of bytes, + // always use one of bytes1 to bytes32 because they are much cheaper + bytes32 name; // short name (up to 32 bytes) + uint voteCount; // number of accumulated votes + } + + address public chairperson; + + mapping(address => Voter) public voters; + + Proposal[] public proposals; + +function () test { + + /** + * @dev Create a new ballot to choose one of 'proposalNames'. + * @param proposalNames names of proposals + */ + constructor(bytes32[] memory proposalNames) { + chairperson = msg.sender; + voters[chairperson].weight = 1; + + for (uint i = 0; i < proposalNames.length; i++) { + // 'Proposal({...})' creates a temporary + // Proposal object and 'proposals.push(...)' + // appends it to the end of 'proposals'. + proposals.push(Proposal({ + name: proposalNames[i], + voteCount: 0 + })); + } + } + + /** + * @dev Give 'voter' the right to vote on this ballot. May only be called by 'chairperson'. + * @param voter address of voter + */ + function giveRightToVote(address voter) public { + require( + msg.sender == chairperson, + "Only chairperson can give right to vote." + ); + require( + !voters[voter].voted, + "The voter already voted." + ); + require(voters[voter].weight == 0); + voters[voter].weight = 1; + } + + /** + * @dev Delegate your vote to the voter 'to'. + * @param to address to which vote is delegated + */ + function delegate(address to) public { + Voter storage sender = voters[msg.sender]; + require(!sender.voted, "You already voted."); + require(to != msg.sender, "Self-delegation is disallowed."); + + while (voters[to].delegate != address(0)) { + to = voters[to].delegate; + + // We found a loop in the delegation, not allowed. + require(to != msg.sender, "Found loop in delegation."); + } + sender.voted = true; + sender.delegate = to; + Voter storage delegate_ = voters[to]; + if (delegate_.voted) { + // If the delegate already voted, + // directly add to the number of votes + proposals[delegate_.vote].voteCount += sender.weight; + } else { + // If the delegate did not vote yet, + // add to her weight. + delegate_.weight += sender.weight; + } + } + + /** + * @dev Give your vote (including votes delegated to you) to proposal 'proposals[proposal].name'. + * @param proposal index of proposal in the proposals array + */ + function vote(uint proposal) public { + Voter storage sender = voters[msg.sender]; + require(sender.weight != 0, "Has no right to vote"); + require(!sender.voted, "Already voted."); + sender.voted = true; + sender.vote = proposal; + + // If 'proposal' is out of the range of the array, + // this will throw automatically and revert all + // changes. + proposals[proposal].voteCount += sender.weight; + } + + /** + * @dev Computes the winning proposal taking all previous votes into account. + * @return winningProposal_ index of winning proposal in the proposals array + */ + function winningProposal() public view + returns (uint winningProposal_) + { + uint winningVoteCount = 0; + for (uint p = 0; p < proposals.length; p++) { + if (proposals[p].voteCount > winningVoteCount) { + winningVoteCount = proposals[p].voteCount; + winningProposal_ = p; + } + } + } + + /** + * @dev Calls winningProposal() function to get the index of the winner contained in the proposals array and then + * @return winnerName_ the name of the winner + */ + function winnerName() public view + returns (bytes32 winnerName_) + { + winnerName_ = proposals[winningProposal()].name; + } +} + +` diff --git a/apps/remix-ide/src/app/files/fileManager.ts b/apps/remix-ide/src/app/files/fileManager.ts index d9789f4512..df40820b27 100644 --- a/apps/remix-ide/src/app/files/fileManager.ts +++ b/apps/remix-ide/src/app/files/fileManager.ts @@ -19,7 +19,7 @@ const profile = { icon: 'assets/img/fileManager.webp', permission: true, version: packageJson.version, - methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile', 'setBatchFiles'], + methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile', 'setBatchFiles'], kind: 'file-system' } const errorMsg = { @@ -704,7 +704,14 @@ class FileManager extends Plugin { return collectList(path) } - isRemixDActive() { + async fileList (dirPath) { + const paths: any = await this.readdir(dirPath) + for( const path in paths) + if(paths[path].isDirectory) delete paths[path] + return Object.keys(paths) + } + + isRemixDActive () { return this.appManager.isActive('remixd') } diff --git a/libs/remix-core-plugin/src/lib/compiler-artefacts.ts b/libs/remix-core-plugin/src/lib/compiler-artefacts.ts index 1198a012db..d35fa36141 100644 --- a/libs/remix-core-plugin/src/lib/compiler-artefacts.ts +++ b/libs/remix-core-plugin/src/lib/compiler-artefacts.ts @@ -4,7 +4,7 @@ import { CompilerAbstract } from '@remix-project/remix-solidity' const profile = { name: 'compilerArtefacts', - methods: ['get', 'addResolvedContract', 'getCompilerAbstract', 'getAllContractDatas', 'getLastCompilationResult'], + methods: ['get', 'addResolvedContract', 'getCompilerAbstract', 'getAllContractDatas', 'getLastCompilationResult', 'getArtefactsByContractName'], events: [], version: '0.0.1' } @@ -72,6 +72,53 @@ export class CompilerArtefacts extends Plugin { return contractsData } + async getArtefactsFromFE (path, contractName) { + const dirList = await this.call('fileManager', 'dirList', path) + if(dirList && dirList.length) { + if(dirList.includes(path + '/artifacts')) { + const fileList = await this.call('fileManager', 'fileList', path + '/artifacts') + const artefactsFilePaths = fileList.filter(filePath => { + const filenameArr = filePath.split('/') + const filename = filenameArr[filenameArr.length - 1] + if (filename === `${contractName}.json` || filename === `${contractName}_metadata.json`) return true + }) + if (artefactsFilePaths && artefactsFilePaths.length) { + const content = await this.call('fileManager', 'readFile', artefactsFilePaths[1]) + const artifacts = JSON.parse(content) + return { abi: artifacts.abi, bytecode: artifacts.data.bytecode.object } + } else { + for (const dirPath of dirList) { + const result = await this.getArtefactsFromFE (dirPath, contractName) + if (result) return result + } + } + } else { + for (const dirPath of dirList) { + const result = await this.getArtefactsFromFE (dirPath, contractName) + if (result) return result + } + } + } else return + } + + async getArtefactsByContractName (contractName) { + const contractsDataByFilename = this.getAllContractDatas() + const contractsData = Object.values(contractsDataByFilename) + if (contractsData && contractsData.length) { + const index = contractsData.findIndex((contractsObj) => Object.keys(contractsObj).includes(contractName)) + if (index !== -1) return { abi: contractsData[index][contractName].abi, bytecode: contractsData[index][contractName].evm.bytecode.object } + else { + const result = await this.getArtefactsFromFE ('contracts', contractName) + if (result) return result + else throw new Error(`Could not find artifacts for ${contractName}. Compile contract to generate artifacts.`) + } + } else { + const result = await this.getArtefactsFromFE ('contracts', contractName) + if (result) return result + else throw new Error(`Could not find artifacts for ${contractName}. Compile contract to generate artifacts.`) + } + } + getCompilerAbstract (file) { return this.compilersArtefactsPerFile[file] } diff --git a/libs/remix-ui/workspace/src/lib/actions/index.ts b/libs/remix-ui/workspace/src/lib/actions/index.ts index ae248d6fa8..12f178348c 100644 --- a/libs/remix-ui/workspace/src/lib/actions/index.ts +++ b/libs/remix-ui/workspace/src/lib/actions/index.ts @@ -256,6 +256,17 @@ export const runScript = async (path: string) => { }) } +export const runScriptWithMocha = async (path: string) => { + const provider = plugin.fileManager.currentFileProvider() + provider.get(path, (error, content: string) => { + if (error) { + return dispatch(displayPopUp(error)) + } + if (content) content = content + '\n' + 'mocha.run()' + plugin.call('scriptRunner', 'execute', content) + }) +} + export const emitContextMenuEvent = async (cmd: customAction) => { await plugin.call(cmd.id, cmd.name, cmd) } diff --git a/libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx b/libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx index 5e6518c2de..b9bad4b170 100644 --- a/libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx +++ b/libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx @@ -12,7 +12,7 @@ declare global { const _paq = window._paq = window._paq || [] //eslint-disable-line export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => { - const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, pushChangesToGist, publishFileToGist, publishFolderToGist, copy, paste, runScript, emit, pageX, pageY, path, type, focus, ...otherProps } = props + const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, pushChangesToGist, publishFileToGist, publishFolderToGist, copy, paste, runScript, runScriptWithMocha, emit, pageX, pageY, path, type, focus, ...otherProps } = props const contextMenuRef = useRef(null) useEffect(() => { contextMenuRef.current.focus() @@ -98,6 +98,10 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => _paq.push(['trackEvent', 'fileExplorer', 'runScript']) runScript(path) break + case 'Run with Mocha': + _paq.push(['trackEvent', 'fileExplorer', 'runScriptWithMocha']) + runScriptWithMocha(path) + break case 'Copy': copy(path, type) break diff --git a/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx b/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx index 5ae7de8d05..d5c0fa8f75 100644 --- a/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx +++ b/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx @@ -221,6 +221,14 @@ export const FileExplorer = (props: FileExplorerProps) => { } } + const runScriptWithMocha = async (path: string) => { + try { + props.dispatchRunScriptWithMocha(path) + } catch (error) { + props.toast('Run script with Mocha failed') + } + } + const emitContextMenuEvent = (cmd: customAction) => { try { props.dispatchEmitContextMenuEvent(cmd) @@ -454,6 +462,7 @@ export const FileExplorer = (props: FileExplorerProps) => { deletePath={deletePath} renamePath={editModeOn} runScript={runScript} + runScriptWithMocha={runScriptWithMocha} copy={handleCopyClick} paste={handlePasteClick} emit={emitContextMenuEvent} diff --git a/libs/remix-ui/workspace/src/lib/contexts/index.ts b/libs/remix-ui/workspace/src/lib/contexts/index.ts index 1aedce98b5..20c9b46633 100644 --- a/libs/remix-ui/workspace/src/lib/contexts/index.ts +++ b/libs/remix-ui/workspace/src/lib/contexts/index.ts @@ -25,6 +25,7 @@ export const FileSystemContext = createContext<{ dispatchCopyFile: (src: string, dest: string) => Promise, dispatchCopyFolder: (src: string, dest: string) => Promise, dispatchRunScript: (path: string) => Promise, + dispatchRunScriptWithMocha: (path: string) => Promise, dispatchEmitContextMenuEvent: (cmd: customAction) => Promise, dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise dispatchHandleExpandPath: (paths: string[]) => Promise diff --git a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx index 904ed7cade..330270cf5e 100644 --- a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx +++ b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx @@ -5,7 +5,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line // eslint-disable-next-line @typescript-eslint/no-unused-vars import { FileSystemContext } from '../contexts' import { browserReducer, browserInitialState } from '../reducers/workspace' -import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile } from '../actions' +import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, runScriptWithMocha, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile } from '../actions' import { Modal, WorkspaceProps } from '../types' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { Workspace } from '../remix-ui-workspace' @@ -103,6 +103,10 @@ export const FileSystemProvider = (props: WorkspaceProps) => { await runScript(path) } + const dispatchRunScriptWithMocha = async (path: string) => { + await runScriptWithMocha(path) + } + const dispatchEmitContextMenuEvent = async (cmd: customAction) => { await emitContextMenuEvent(cmd) } @@ -212,6 +216,7 @@ export const FileSystemProvider = (props: WorkspaceProps) => { dispatchCopyFile, dispatchCopyFolder, dispatchRunScript, + dispatchRunScriptWithMocha, dispatchEmitContextMenuEvent, dispatchHandleClickFile, dispatchHandleExpandPath diff --git a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx index a51224f86c..a5a84e1e1f 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -198,6 +198,7 @@ export function Workspace () { dispatchCopyFolder={global.dispatchCopyFolder} dispatchPublishToGist={global.dispatchPublishToGist} dispatchRunScript={global.dispatchRunScript} + dispatchRunScriptWithMocha={global.dispatchRunScriptWithMocha} dispatchEmitContextMenuEvent={global.dispatchEmitContextMenuEvent} dispatchHandleClickFile={global.dispatchHandleClickFile} dispatchSetFocusElement={global.dispatchSetFocusElement} @@ -233,6 +234,7 @@ export function Workspace () { dispatchCopyFolder={global.dispatchCopyFolder} dispatchPublishToGist={global.dispatchPublishToGist} dispatchRunScript={global.dispatchRunScript} + dispatchRunScriptWithMocha={global.dispatchRunScriptWithMocha} dispatchEmitContextMenuEvent={global.dispatchEmitContextMenuEvent} dispatchHandleClickFile={global.dispatchHandleClickFile} dispatchSetFocusElement={global.dispatchSetFocusElement} diff --git a/libs/remix-ui/workspace/src/lib/types/index.ts b/libs/remix-ui/workspace/src/lib/types/index.ts index bf4b24d661..5db5bd0a99 100644 --- a/libs/remix-ui/workspace/src/lib/types/index.ts +++ b/libs/remix-ui/workspace/src/lib/types/index.ts @@ -78,6 +78,7 @@ export interface FileExplorerProps { dispatchCopyFile: (src: string, dest: string) => Promise, dispatchCopyFolder: (src: string, dest: string) => Promise, dispatchRunScript: (path: string) => Promise, + dispatchRunScriptWithMocha: (path: string) => Promise, dispatchPublishToGist: (path?: string, type?: string) => Promise, dispatchEmitContextMenuEvent: (cmd: customAction) => Promise, dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise, @@ -108,6 +109,7 @@ export interface FileExplorerContextMenuProps { publishFolderToGist?: (path?: string, type?: string) => void, publishFileToGist?: (path?: string, type?: string) => void, runScript?: (path: string) => void, + runScriptWithMocha?: (path: string) => void, emit?: (cmd: customAction) => void, pageX: number, pageY: number, diff --git a/libs/remix-ui/workspace/src/lib/utils/index.ts b/libs/remix-ui/workspace/src/lib/utils/index.ts index 0dd70b14bc..f2558c1b2c 100644 --- a/libs/remix-ui/workspace/src/lib/utils/index.ts +++ b/libs/remix-ui/workspace/src/lib/utils/index.ts @@ -30,6 +30,12 @@ export const contextMenuActions: MenuItems = [{ extension: ['.js'], multiselect: false, label: '' +}, { + id: 'runWithMocha', + name: 'Run with Mocha', + extension: ['.js'], + multiselect: false, + label: '' }, { id: 'pushChangesToGist', name: 'Push changes to gist', diff --git a/package.json b/package.json index d688fc5c17..3585888453 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "nightwatch_local_verticalIconscontextmenu": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/verticalIconsPanel.test.js --env=chrome", "nightwatch_local_pluginApi": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/plugin_api_*.js --env=chrome", "nightwatch_local_migrate_filesystem": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/migrateFileSystem.test.js --env=chrome", + "nightwatch_local_stress_editor": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/stress.editor.js --env=chromeDesktop", "onchange": "onchange apps/remix-ide/build/app.js -- npm-run-all lint", "remixd": "nx build remixd && chmod +x dist/libs/remixd/src/bin/remixd.js && dist/libs/remixd/src/bin/remixd.js -s ./apps/remix-ide/contracts --remix-ide http://127.0.0.1:8080", "selenium": "selenium-standalone start",