Merge branch 'master' of https://github.com/ethereum/remix-project into popuppanelfix
commit
86b012f5bd
@ -0,0 +1,38 @@ |
|||||||
|
'use strict' |
||||||
|
import { NightwatchBrowser } from 'nightwatch' |
||||||
|
import init from '../helpers/init' |
||||||
|
|
||||||
|
declare global { |
||||||
|
interface Window { testplugin: { name: string, url: string }; } |
||||||
|
} |
||||||
|
|
||||||
|
const tests = { |
||||||
|
'@disabled': true, |
||||||
|
before: function (browser: NightwatchBrowser, done: VoidFunction) { |
||||||
|
init(browser, done, null) |
||||||
|
}, |
||||||
|
|
||||||
|
'Should load contract verification plugin #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.clickLaunchIcon('pluginManager') |
||||||
|
.scrollAndClick('[data-id="pluginManagerComponentActivateButtoncontract-verification"]') |
||||||
|
.clickLaunchIcon('contract-verification') |
||||||
|
.pause(5000) |
||||||
|
.frame(0) |
||||||
|
.waitForElementVisible('*[data-id="VerifyDescription"]') |
||||||
|
}, |
||||||
|
|
||||||
|
'Should select a chain by searching #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.click('[data-id="chainDropdownbox"]') |
||||||
|
.sendKeys('[data-id="chainDropdownbox"]', 's') |
||||||
|
.sendKeys('[data-id="chainDropdownbox"]', 'c') |
||||||
|
.sendKeys('[data-id="chainDropdownbox"]', 'r') |
||||||
|
.click('[data-id="534351"]') |
||||||
|
.assert.attributeContains('[data-id="chainDropdownbox"]', 'value', "Scroll Sepolia Testnet (534351)") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
...tests |
||||||
|
}; |
@ -0,0 +1,433 @@ |
|||||||
|
'use strict' |
||||||
|
import { NightwatchBrowser } from 'nightwatch' |
||||||
|
import init from '../helpers/init' |
||||||
|
import examples from '../examples/example-contracts' |
||||||
|
|
||||||
|
const passphrase = process.env.account_passphrase |
||||||
|
const password = process.env.account_password |
||||||
|
const extension_id = 'nkbihfbeogaeaoehlefnkodbefgpgknn' |
||||||
|
const extension_url = `chrome-extension://${extension_id}/home.html` |
||||||
|
|
||||||
|
const checkBrowserIsChrome = function (browser: NightwatchBrowser) { |
||||||
|
return browser.browserName.indexOf('chrome') > -1 |
||||||
|
} |
||||||
|
|
||||||
|
const checkAlerts = function (browser: NightwatchBrowser) { |
||||||
|
browser.isVisible({ |
||||||
|
selector: '//*[contains(.,"not have enough")]', |
||||||
|
locateStrategy: 'xpath', |
||||||
|
suppressNotFoundErrors: true, |
||||||
|
timeout: 3000 |
||||||
|
}, (okVisible) => { |
||||||
|
if (okVisible.value) { |
||||||
|
browser.assert.fail('Not enough ETH in test account!!') |
||||||
|
browser.end() |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const localsCheck = { |
||||||
|
to: { |
||||||
|
value: '0x4B0897B0513FDC7C541B6D9D7E929C4E5364D2DB', |
||||||
|
type: 'address' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const tests = { |
||||||
|
'@disabled': true, |
||||||
|
before: function (browser: NightwatchBrowser, done: VoidFunction) { |
||||||
|
init(browser, done) |
||||||
|
}, |
||||||
|
|
||||||
|
'@sources': function () { |
||||||
|
return sources |
||||||
|
}, |
||||||
|
|
||||||
|
'Should connect to Sepolia Test Network using MetaMask #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser.waitForElementPresent('*[data-id="remixIdeSidePanel"]') |
||||||
|
.setupMetamask(passphrase, password) |
||||||
|
.useCss().switchBrowserTab(0) |
||||||
|
.refreshPage() |
||||||
|
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) |
||||||
|
.click('*[data-id="landingPageStartSolidity"]') |
||||||
|
.clickLaunchIcon('udapp') |
||||||
|
.switchEnvironment('injected-MetaMask') |
||||||
|
.waitForElementPresent('*[data-id="settingsNetworkEnv"]') |
||||||
|
.assert.containsText('*[data-id="settingsNetworkEnv"]', 'Sepolia (11155111) network') |
||||||
|
.pause(5000) |
||||||
|
.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { |
||||||
|
browser |
||||||
|
.hideMetaMaskPopup() |
||||||
|
.waitForElementVisible('*[data-testid="page-container-footer-next"]', 60000) |
||||||
|
.click('*[data-testid="page-container-footer-next"]') // this connects the metamask account to remix
|
||||||
|
.pause(2000) |
||||||
|
.waitForElementVisible('*[data-testid="page-container-footer-next"]', 60000) |
||||||
|
.click('*[data-testid="page-container-footer-next"]') |
||||||
|
}) |
||||||
|
.switchBrowserTab(0) // back to remix
|
||||||
|
}, |
||||||
|
|
||||||
|
'Should add a contract file #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]') |
||||||
|
.clickLaunchIcon('filePanel') |
||||||
|
.addFile('Greet.sol', sources[0]['Greet.sol']) |
||||||
|
.clickLaunchIcon('udapp') |
||||||
|
.waitForElementVisible('*[data-id="Deploy - transact (not payable)"]', 45000) // wait for the contract to compile
|
||||||
|
}, |
||||||
|
|
||||||
|
'Should deploy contract on Sepolia Test Network using MetaMask #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser.clearConsole().waitForElementPresent('*[data-id="runTabSelectAccount"] option', 45000) |
||||||
|
.clickLaunchIcon('filePanel') |
||||||
|
.openFile('Greet.sol') |
||||||
|
.clickLaunchIcon('udapp') |
||||||
|
.waitForElementPresent('*[data-id="Deploy - transact (not payable)"]') |
||||||
|
.click('*[data-id="Deploy - transact (not payable)"]') |
||||||
|
.pause(5000) |
||||||
|
.clearConsole() |
||||||
|
.perform((done) => { |
||||||
|
browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { |
||||||
|
checkAlerts(browser) |
||||||
|
browser |
||||||
|
.maximizeWindow() |
||||||
|
.hideMetaMaskPopup() |
||||||
|
.waitForElementPresent('[data-testid="page-container-footer-next"]') |
||||||
|
.click('[data-testid="page-container-footer-next"]') // approve the tx
|
||||||
|
.switchBrowserTab(0) // back to remix
|
||||||
|
.waitForElementContainsText('*[data-id="terminalJournal"]', 'view on etherscan', 60000) |
||||||
|
.waitForElementContainsText('*[data-id="terminalJournal"]', 'from: 0x76a...2708f', 60000) |
||||||
|
.perform(() => done()) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}, |
||||||
|
'Should run low level interaction (fallback function) on Sepolia Test Network using MetaMask #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser.clearConsole().waitForElementPresent('*[data-id="remixIdeSidePanel"]') |
||||||
|
.clickInstance(0) |
||||||
|
.clearConsole() |
||||||
|
.waitForElementPresent('*[data-id="pluginManagerSettingsDeployAndRunLLTxSendTransaction"]') |
||||||
|
.click('*[data-id="pluginManagerSettingsDeployAndRunLLTxSendTransaction"]') |
||||||
|
.perform((done) => { |
||||||
|
browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { |
||||||
|
browser |
||||||
|
.maximizeWindow() |
||||||
|
.hideMetaMaskPopup() |
||||||
|
.pause(3000) |
||||||
|
.scrollAndClick('[data-testid="page-container-footer-next"]') |
||||||
|
.pause(2000) |
||||||
|
.switchBrowserTab(0) // back to remix
|
||||||
|
.waitForElementVisible({ |
||||||
|
locateStrategy: 'xpath', |
||||||
|
selector: "//span[@class='text-log' and contains(., 'transact to HelloWorld.(fallback) pending')]" |
||||||
|
}) |
||||||
|
.waitForElementContainsText('*[data-id="terminalJournal"]', 'view on etherscan', 60000) |
||||||
|
.waitForElementContainsText('*[data-id="terminalJournal"]', 'from: 0x76a...2708f', 60000) |
||||||
|
.perform(() => done()) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}, |
||||||
|
'Should run transaction (greet function) on Sepolia Test Network using MetaMask #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser.clearConsole().waitForElementPresent('*[data-id="remixIdeSidePanel"]') |
||||||
|
.clearConsole() |
||||||
|
.waitForElementPresent('*[data-title="string _message"]') |
||||||
|
.setValue('*[data-title="string _message"]', 'test') |
||||||
|
.waitForElementVisible('*[data-id="greet - transact (not payable)"]') |
||||||
|
.click('*[data-id="greet - transact (not payable)"]') |
||||||
|
.perform((done) => { |
||||||
|
browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { |
||||||
|
browser |
||||||
|
.maximizeWindow() |
||||||
|
.hideMetaMaskPopup() |
||||||
|
.pause(3000) |
||||||
|
.scrollAndClick('[data-testid="page-container-footer-next"]') |
||||||
|
.pause(2000) |
||||||
|
.switchBrowserTab(0) // back to remix
|
||||||
|
.waitForElementVisible({ |
||||||
|
locateStrategy: 'xpath', |
||||||
|
selector: "//span[@class='text-log' and contains(., 'transact to HelloWorld.greet pending')]" |
||||||
|
}) |
||||||
|
.waitForElementContainsText('*[data-id="terminalJournal"]', 'view on etherscan', 60000) |
||||||
|
.waitForElementContainsText('*[data-id="terminalJournal"]', 'from: 0x76a...2708f', 60000) |
||||||
|
.perform(() => done()) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
'Should deploy faulty contract on Sepolia Test Network using MetaMask and show error in terminal #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.clearConsole() |
||||||
|
.clickLaunchIcon('filePanel') |
||||||
|
.addFile('faulty.sol', sources[0]['faulty.sol']) |
||||||
|
.clickLaunchIcon('udapp') |
||||||
|
.waitForElementPresent('*[data-id="Deploy - transact (not payable)"]') |
||||||
|
.click('*[data-id="Deploy - transact (not payable)"]') |
||||||
|
.pause(5000) |
||||||
|
.waitForElementVisible('*[data-id="udappNotifyModalDialogModalBody-react"]', 60000) |
||||||
|
.click('[data-id="udappNotify-modal-footer-cancel-react"]') |
||||||
|
.waitForElementVisible({ |
||||||
|
locateStrategy: 'xpath', |
||||||
|
selector: "//span[@class='text-log' and contains(., 'errored')]" |
||||||
|
}) |
||||||
|
}, |
||||||
|
'Should deploy contract on Sepolia Test Network using MetaMask again #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser.clearConsole().waitForElementPresent('*[data-id="runTabSelectAccount"] option', 45000) |
||||||
|
.clickLaunchIcon('filePanel') |
||||||
|
.openFile('Greet.sol') |
||||||
|
.clickLaunchIcon('udapp') |
||||||
|
.waitForElementPresent('*[data-id="Deploy - transact (not payable)"]') |
||||||
|
.click('*[data-id="Deploy - transact (not payable)"]') |
||||||
|
.pause(5000) |
||||||
|
.clearConsole() |
||||||
|
.perform((done) => { |
||||||
|
browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { |
||||||
|
checkAlerts(browser) |
||||||
|
browser |
||||||
|
.maximizeWindow() |
||||||
|
.hideMetaMaskPopup() |
||||||
|
.waitForElementPresent('[data-testid="page-container-footer-next"]') |
||||||
|
.click('[data-testid="page-container-footer-next"]') // approve the tx
|
||||||
|
.switchBrowserTab(0) // back to remix
|
||||||
|
.waitForElementContainsText('*[data-id="terminalJournal"]', 'view on etherscan', 60000) |
||||||
|
.waitForElementContainsText('*[data-id="terminalJournal"]', 'from: 0x76a...2708f', 60000) |
||||||
|
.perform(() => done()) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// main network tests
|
||||||
|
'Should connect to Ethereum Main Network using MetaMask #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser.waitForElementPresent('*[data-id="remixIdeSidePanel"]') |
||||||
|
.switchBrowserTab(1) |
||||||
|
.click('[data-testid="network-display"]') |
||||||
|
.click('div[data-testid="Ethereum Mainnet"]') // switch to mainnet
|
||||||
|
.useCss().switchBrowserTab(0) |
||||||
|
.refreshPage() |
||||||
|
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) |
||||||
|
.click('*[data-id="landingPageStartSolidity"]') |
||||||
|
.clickLaunchIcon('udapp') |
||||||
|
.switchEnvironment('injected-MetaMask') |
||||||
|
.waitForElementPresent('*[data-id="settingsNetworkEnv"]') |
||||||
|
.assert.containsText('*[data-id="settingsNetworkEnv"]', 'Main (1) network') |
||||||
|
}, |
||||||
|
|
||||||
|
'Should deploy contract on Ethereum Main Network using MetaMask #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser.waitForElementPresent('*[data-id="runTabSelectAccount"] option') |
||||||
|
.clickLaunchIcon('filePanel') |
||||||
|
.addFile('Greet.sol', sources[0]['Greet.sol']) |
||||||
|
.clickLaunchIcon('udapp') |
||||||
|
.waitForElementPresent('*[data-id="Deploy - transact (not payable)"]') |
||||||
|
.click('*[data-id="Deploy - transact (not payable)"]') |
||||||
|
.waitForElementVisible('*[data-id="udappNotifyModalDialogModalBody-react"]', 65000) |
||||||
|
.modalFooterOKClick('udappNotify') |
||||||
|
.pause(10000) |
||||||
|
.assert.containsText('*[data-id="udappNotifyModalDialogModalBody-react"]', 'You are about to create a transaction on Main Network. Confirm the details to send the info to your provider.') |
||||||
|
.modalFooterCancelClick('udappNotify') |
||||||
|
}, |
||||||
|
// debug transaction
|
||||||
|
'Should deploy Ballot to Sepolia using metamask #group1 #flaky': function (browser: NightwatchBrowser) { |
||||||
|
browser.waitForElementPresent('*[data-id="remixIdeSidePanel"]') |
||||||
|
.switchBrowserTab(1) |
||||||
|
.click('[data-testid="network-display"]') |
||||||
|
.click('div[data-testid="Sepolia"]') // switch to sepolia
|
||||||
|
.useCss().switchBrowserTab(0) |
||||||
|
.addFile('BallotTest.sol', examples.ballot) |
||||||
|
.clickLaunchIcon('udapp') |
||||||
|
.clearConsole() |
||||||
|
.clearTransactions() |
||||||
|
.clickLaunchIcon('udapp') |
||||||
|
.waitForElementVisible('input[placeholder="bytes32[] proposalNames"]') |
||||||
|
.pause(2000) |
||||||
|
.setValue('input[placeholder="bytes32[] proposalNames"]', '["0x48656c6c6f20576f726c64210000000000000000000000000000000000000000"]') |
||||||
|
.pause(1000) |
||||||
|
.click('*[data-id="Deploy - transact (not payable)"]') // deploy ballot
|
||||||
|
.pause(1000) |
||||||
|
.waitForElementVisible({ |
||||||
|
locateStrategy: 'xpath', |
||||||
|
selector: "//span[@class='text-log' and contains(., 'creation of')]" |
||||||
|
}) |
||||||
|
.waitForElementVisible({ |
||||||
|
locateStrategy: 'xpath', |
||||||
|
selector: "//span[@class='text-log' and contains(., 'pending')]" |
||||||
|
}) |
||||||
|
.perform((done) => { |
||||||
|
browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { |
||||||
|
browser |
||||||
|
.maximizeWindow() |
||||||
|
.hideMetaMaskPopup() |
||||||
|
.pause(3000) |
||||||
|
.waitForElementPresent('[data-testid="page-container-footer-next"]') |
||||||
|
.scrollAndClick('[data-testid="page-container-footer-next"]') |
||||||
|
.pause(2000) |
||||||
|
.switchBrowserTab(0) // back to remix
|
||||||
|
.waitForElementContainsText('*[data-id="terminalJournal"]', 'view on etherscan', 60000) |
||||||
|
.waitForElementContainsText('*[data-id="terminalJournal"]', 'from: 0x76a...2708f', 60000) |
||||||
|
.perform(() => done()) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
'do transaction #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser.waitForElementPresent('*[data-id="universalDappUiContractActionWrapper"]', 60000) |
||||||
|
.clearConsole() |
||||||
|
.clickInstance(0) |
||||||
|
.clickFunction('delegate - transact (not payable)', { types: 'address to', values: '"0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"' }) |
||||||
|
.pause(5000) |
||||||
|
.perform((done) => { // call delegate
|
||||||
|
browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { |
||||||
|
browser |
||||||
|
.maximizeWindow() |
||||||
|
.hideMetaMaskPopup() |
||||||
|
.pause(5000) |
||||||
|
.waitForElementPresent('[data-testid="page-container-footer-next"]') |
||||||
|
.scrollAndClick('[data-testid="page-container-footer-next"]') |
||||||
|
.pause(2000) |
||||||
|
.switchBrowserTab(0) // back to remix
|
||||||
|
.waitForElementContainsText('*[data-id="terminalJournal"]', 'view on etherscan', 60000) |
||||||
|
.waitForElementContainsText('*[data-id="terminalJournal"]', 'from: 0x76a...2708f', 60000) |
||||||
|
.perform(() => done()) |
||||||
|
}) |
||||||
|
}) |
||||||
|
.testFunction('last', |
||||||
|
{ |
||||||
|
status: '0x1 Transaction mined and execution succeed', |
||||||
|
'decoded input': { 'address to': '0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB' } |
||||||
|
}) |
||||||
|
}, |
||||||
|
'Should debug Sepolia transaction with source highlighting MetaMask #group1': function (browser: NightwatchBrowser) { |
||||||
|
let txhash |
||||||
|
browser.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) |
||||||
|
.clickLaunchIcon('pluginManager') // load debugger and source verification
|
||||||
|
.clickLaunchIcon('udapp') |
||||||
|
.perform((done) => { |
||||||
|
browser.getLastTransactionHash((hash) => { |
||||||
|
txhash = hash |
||||||
|
done() |
||||||
|
}) |
||||||
|
}) |
||||||
|
.pause(5000) |
||||||
|
.perform((done) => { |
||||||
|
browser |
||||||
|
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) |
||||||
|
.clickLaunchIcon('debugger') |
||||||
|
.waitForElementVisible('*[data-id="debuggerTransactionInput"]') |
||||||
|
.setValue('*[data-id="debuggerTransactionInput"]', txhash) // debug tx
|
||||||
|
.pause(2000) |
||||||
|
.click('*[data-id="debuggerTransactionStartButton"]') |
||||||
|
.waitForElementVisible('*[data-id="treeViewDivto"]', 30000) |
||||||
|
.checkVariableDebug('soliditylocals', localsCheck) |
||||||
|
.perform(() => done()) |
||||||
|
}) |
||||||
|
|
||||||
|
}, |
||||||
|
'Call web3.eth.getAccounts() using Injected Provider (Metamask) #group1': function (browser: NightwatchBrowser) { |
||||||
|
if (!checkBrowserIsChrome(browser)) return |
||||||
|
browser |
||||||
|
.executeScriptInTerminal('web3.eth.getAccounts()') |
||||||
|
.journalLastChildIncludes('["0x76a3ABb5a12dcd603B52Ed22195dED17ee82708f"]') |
||||||
|
}, |
||||||
|
// EIP 712 tests
|
||||||
|
'Test EIP 712 Signature with Injected Provider (Metamask) #group1': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.clickLaunchIcon('udapp') |
||||||
|
.waitForElementPresent('i[id="remixRunSignMsg"]') |
||||||
|
.click('i[id="remixRunSignMsg"]') |
||||||
|
.waitForElementVisible('*[data-id="signMessageTextarea"]', 120000) |
||||||
|
.click('*[data-id="sign-eip-712"]') |
||||||
|
.waitForElementVisible('*[data-id="udappNotify-modal-footer-ok-react"]') |
||||||
|
.modalFooterOKClick('udappNotify') |
||||||
|
.pause(1000) |
||||||
|
.getEditorValue((content) => { |
||||||
|
browser.assert.ok(content.indexOf('"primaryType": "AuthRequest",') !== -1, 'EIP 712 data file must be opened') |
||||||
|
}) |
||||||
|
.setEditorValue(JSON.stringify(EIP712_Example, null, '\t')) |
||||||
|
.pause(5000) |
||||||
|
.clickLaunchIcon('filePanel') |
||||||
|
.rightClick('li[data-id="treeViewLitreeViewItemEIP-712-data.json"]') |
||||||
|
.click('*[data-id="contextMenuItemsignTypedData"]') |
||||||
|
.pause(1000) |
||||||
|
.perform((done) => { // call delegate
|
||||||
|
browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => { |
||||||
|
browser |
||||||
|
.maximizeWindow() |
||||||
|
.hideMetaMaskPopup() |
||||||
|
.pause(1000) |
||||||
|
.waitForElementPresent('[data-testid="page-container-footer-next"]') |
||||||
|
.scrollAndClick('button[data-testid="page-container-footer-next"]') // confirm
|
||||||
|
.switchBrowserTab(0) // back to remix
|
||||||
|
.perform(() => done()) |
||||||
|
}) |
||||||
|
}) |
||||||
|
.pause(1000) |
||||||
|
.journalChildIncludes('0xec72bbabeb47a3a766af449674a45a91a6e94e35ebf0ae3c644b66def7bd387f1c0b34d970c9f4a1e9398535e5860b35e82b2a8931b7c9046b7766a53e66db3d1b') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const branch = process.env.CIRCLE_BRANCH |
||||||
|
const runTestsConditions = branch && (branch === 'master' || branch === 'remix_live' || branch.includes('remix_beta') || branch.includes('metamask')) |
||||||
|
|
||||||
|
if (!checkBrowserIsChrome(browser)) { |
||||||
|
module.exports = {} |
||||||
|
} else { |
||||||
|
module.exports = { |
||||||
|
...(branch ? (runTestsConditions ? tests : {}) : tests) |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
const EIP712_Example = { |
||||||
|
domain: { |
||||||
|
chainId: 11155111, |
||||||
|
name: "Example App", |
||||||
|
verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", |
||||||
|
version: "1", |
||||||
|
}, |
||||||
|
message: { |
||||||
|
prompt: "Welcome! In order to authenticate to this website, sign this request and your public address will be sent to the server in a verifiable way.", |
||||||
|
createdAt: 1718570375196, |
||||||
|
}, |
||||||
|
primaryType: 'AuthRequest', |
||||||
|
types: { |
||||||
|
EIP712Domain: [ |
||||||
|
{ name: 'name', type: 'string' }, |
||||||
|
{ name: 'version', type: 'string' }, |
||||||
|
{ name: 'chainId', type: 'uint256' }, |
||||||
|
{ name: 'verifyingContract', type: 'address' }, |
||||||
|
], |
||||||
|
AuthRequest: [ |
||||||
|
{ name: 'prompt', type: 'string' }, |
||||||
|
{ name: 'createdAt', type: 'uint256' }, |
||||||
|
], |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
const sources = [ |
||||||
|
{ |
||||||
|
'Greet.sol': { |
||||||
|
content: |
||||||
|
` |
||||||
|
pragma solidity ^0.8.0; |
||||||
|
contract HelloWorld { |
||||||
|
string public message; |
||||||
|
|
||||||
|
fallback () external { |
||||||
|
message = 'Hello World!'; |
||||||
|
} |
||||||
|
|
||||||
|
function greet(string memory _message) public { |
||||||
|
message = _message; |
||||||
|
} |
||||||
|
}` |
||||||
|
}, |
||||||
|
'faulty.sol': { |
||||||
|
content: `// SPDX-License-Identifier: GPL-3.0
|
||||||
|
|
||||||
|
pragma solidity >=0.8.2 <0.9.0; |
||||||
|
|
||||||
|
contract Test { |
||||||
|
error O_o(uint256); |
||||||
|
constructor() { |
||||||
|
revert O_o(block.timestamp); |
||||||
|
} |
||||||
|
}` |
||||||
|
} |
||||||
|
} |
||||||
|
] |
@ -0,0 +1,116 @@ |
|||||||
|
'use strict' |
||||||
|
import { NightwatchBrowser } from 'nightwatch' |
||||||
|
import init from '../helpers/init' |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
|
||||||
|
before: function (browser: NightwatchBrowser, done: VoidFunction) { |
||||||
|
init(browser, done, 'http://127.0.0.1:8080', false) |
||||||
|
}, |
||||||
|
'Should load default script runner': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.clickLaunchIcon('scriptRunnerBridge') |
||||||
|
.waitForElementVisible('[data-id="sr-loaded-default"]') |
||||||
|
.waitForElementVisible('[data-id="dependency-ethers-^5"]') |
||||||
|
.waitForElementVisible('[data-id="sr-toggle-ethers6"]') |
||||||
|
}, |
||||||
|
'Should load script runner ethers6': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.click('[data-id="sr-toggle-ethers6"]') |
||||||
|
.waitForElementVisible('[data-id="sr-loaded-ethers6"]') |
||||||
|
.waitForElementPresent('[data-id="dependency-ethers-^6"]') |
||||||
|
}, |
||||||
|
'Should have config file in .remix/script.config.json': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.clickLaunchIcon('filePanel') |
||||||
|
.waitForElementVisible('[data-path=".remix"]') |
||||||
|
.waitForElementVisible('[data-id="treeViewDivDraggableItem.remix/script.config.json"]') |
||||||
|
.openFile('.remix/script.config.json') |
||||||
|
}, |
||||||
|
'check config file content': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.getEditorValue((content) => { |
||||||
|
console.log(JSON.parse(content)) |
||||||
|
const parsed = JSON.parse(content) |
||||||
|
browser.assert.ok(parsed.defaultConfig === 'ethers6', 'config file content is correct') |
||||||
|
}) |
||||||
|
}, |
||||||
|
'execute ethers6 script': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.click('*[data-id="treeViewUltreeViewMenu"]') // make sure we create the file at the root folder
|
||||||
|
.addFile('deployWithEthersJs.js', { content: deployWithEthersJs }) |
||||||
|
.pause(1000) |
||||||
|
.click('[data-id="treeViewDivtreeViewItemcontracts"]') |
||||||
|
.openFile('contracts/2_Owner.sol') |
||||||
|
.clickLaunchIcon('solidity') |
||||||
|
.click('*[data-id="compilerContainerCompileBtn"]') |
||||||
|
.executeScriptInTerminal('remix.execute(\'deployWithEthersJs.js\')') |
||||||
|
.waitForElementContainsText('*[data-id="terminalJournal"]', '0xd9145CCE52D386f254917e481eB44e9943F39138', 60000) |
||||||
|
}, |
||||||
|
'switch workspace it should be default again': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.clickLaunchIcon('filePanel') |
||||||
|
.pause(2000) |
||||||
|
.waitForElementVisible('*[data-id="workspacesMenuDropdown"]') |
||||||
|
.click('*[data-id="workspacesMenuDropdown"]') |
||||||
|
.click('*[data-id="workspacecreate"]') |
||||||
|
.waitForElementPresent('*[data-id="create-semaphore"]') |
||||||
|
.scrollAndClick('*[data-id="create-semaphore"]') |
||||||
|
.modalFooterOKClick('TemplatesSelection') |
||||||
|
.clickLaunchIcon('scriptRunnerBridge') |
||||||
|
.waitForElementVisible('[data-id="sr-loaded-default"]') |
||||||
|
.waitForElementVisible('[data-id="dependency-ethers-^5"]') |
||||||
|
.waitForElementVisible('[data-id="sr-toggle-ethers6"]') |
||||||
|
}, |
||||||
|
'switch to default workspace that should be on ethers6': function (browser: NightwatchBrowser) { |
||||||
|
browser |
||||||
|
.clickLaunchIcon('filePanel') |
||||||
|
.switchWorkspace('default_workspace') |
||||||
|
.clickLaunchIcon('scriptRunnerBridge') |
||||||
|
.waitForElementVisible('[data-id="sr-loaded-ethers6"]') |
||||||
|
.waitForElementPresent('[data-id="dependency-ethers-^6"]') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
const deployWithEthersJs = ` |
||||||
|
import { ethers } from 'ethers' |
||||||
|
|
||||||
|
/** |
||||||
|
* Deploy the given contract |
||||||
|
* @param {string} contractName name of the contract to deploy |
||||||
|
* @param {Array<any>} args list of constructor' parameters |
||||||
|
* @param {Number} accountIndex account index from the exposed account |
||||||
|
* @return {Contract} deployed contract |
||||||
|
*
|
||||||
|
*/ |
||||||
|
const deploy = async (contractName: string, args: Array<any>, accountIndex?: number): Promise<ethers.Contract> => { |
||||||
|
|
||||||
|
console.log(\`deploying \${contractName}\`)
|
||||||
|
// Note that the script needs the ABI which is generated from the compilation artifact.
|
||||||
|
// Make sure contract is compiled and artifacts are generated
|
||||||
|
const artifactsPath = \`contracts/artifacts/\${contractName}.json\` // Change this for different path
|
||||||
|
|
||||||
|
const metadata = JSON.parse(await remix.call('fileManager', 'getFile', artifactsPath)) |
||||||
|
// 'web3Provider' is a remix global variable object
|
||||||
|
|
||||||
|
const signer = await (new ethers.BrowserProvider(web3Provider)).getSigner(accountIndex) |
||||||
|
|
||||||
|
const factory = new ethers.ContractFactory(metadata.abi, metadata.data.bytecode.object, signer) |
||||||
|
|
||||||
|
const contract = await factory.deploy(...args) |
||||||
|
|
||||||
|
// The contract is NOT deployed yet; we must wait until it is mined
|
||||||
|
await contract.waitForDeployment() |
||||||
|
return contract |
||||||
|
} |
||||||
|
|
||||||
|
(async () => { |
||||||
|
try { |
||||||
|
const contract = await deploy('Owner', []) |
||||||
|
|
||||||
|
console.log(\`address: \${await contract.getAddress()}\`)
|
||||||
|
} catch (e) { |
||||||
|
console.log(e.message) |
||||||
|
} |
||||||
|
})()` |
@ -0,0 +1,33 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
TESTFILES=$(grep -IRiL "\'@disabled\': \?true" "dist/apps/remix-ide-e2e/src/tests" | grep "metamask" | sort ) |
||||||
|
|
||||||
|
# count test files |
||||||
|
fileCount=$(grep -IRiL "\'@disabled\': \?true" "dist/apps/remix-ide-e2e/src/tests" | grep "metamask" | wc -l ) |
||||||
|
# if fileCount is 0 |
||||||
|
if [ $fileCount -eq 0 ] |
||||||
|
then |
||||||
|
echo "No metamask tests found" |
||||||
|
exit 0 |
||||||
|
fi |
||||||
|
|
||||||
|
BUILD_ID=${CIRCLE_BUILD_NUM:-${TRAVIS_JOB_NUMBER}} |
||||||
|
echo "$BUILD_ID" |
||||||
|
TEST_EXITCODE=0 |
||||||
|
|
||||||
|
npx ganache & |
||||||
|
npx http-server -p 9090 --cors='*' ./node_modules & |
||||||
|
yarn run serve:production & |
||||||
|
sleep 5 |
||||||
|
|
||||||
|
for TESTFILE in $TESTFILES; do |
||||||
|
npx nightwatch --config dist/apps/remix-ide-e2e/nightwatch-${1}.js $TESTFILE --env=$1 || TEST_EXITCODE=1 |
||||||
|
done |
||||||
|
|
||||||
|
echo "$TEST_EXITCODE" |
||||||
|
if [ "$TEST_EXITCODE" -eq 1 ] |
||||||
|
then |
||||||
|
exit 1 |
||||||
|
fi |
@ -0,0 +1,399 @@ |
|||||||
|
import { IframePlugin, IframeProfile, ViewPlugin } from '@remixproject/engine-web' |
||||||
|
import * as packageJson from '../../../../../package.json' |
||||||
|
import React from 'react' // eslint-disable-line
|
||||||
|
import { customScriptRunnerConfig, ProjectConfiguration, ScriptRunnerConfig, ScriptRunnerUI } from '@remix-scriptrunner' // eslint-disable-line
|
||||||
|
import { Profile } from '@remixproject/plugin-utils' |
||||||
|
import { Engine, Plugin } from '@remixproject/engine' |
||||||
|
import axios from 'axios' |
||||||
|
import { AppModal } from '@remix-ui/app' |
||||||
|
import { isArray } from 'lodash' |
||||||
|
import { PluginViewWrapper } from '@remix-ui/helper' |
||||||
|
import { CustomRemixApi } from '@remix-api' |
||||||
|
|
||||||
|
const profile = { |
||||||
|
name: 'scriptRunnerBridge', |
||||||
|
displayName: 'Script configuration', |
||||||
|
methods: ['execute'], |
||||||
|
events: ['log', 'info', 'warn', 'error'], |
||||||
|
icon: 'assets/img/solid-gear-circle-play.svg', |
||||||
|
description: 'Configure the dependencies for running scripts.', |
||||||
|
kind: '', |
||||||
|
location: 'sidePanel', |
||||||
|
version: packageJson.version, |
||||||
|
maintainedBy: 'Remix' |
||||||
|
} |
||||||
|
|
||||||
|
const configFileName = '.remix/script.config.json' |
||||||
|
|
||||||
|
let baseUrl = 'https://remix-project-org.github.io/script-runner-generator' |
||||||
|
const customBuildUrl = 'http://localhost:4000/build' // this will be used when the server is ready
|
||||||
|
|
||||||
|
interface IScriptRunnerState { |
||||||
|
customConfig: customScriptRunnerConfig |
||||||
|
configurations: ProjectConfiguration[] |
||||||
|
activeConfig: ProjectConfiguration |
||||||
|
enableCustomScriptRunner: boolean |
||||||
|
} |
||||||
|
|
||||||
|
export class ScriptRunnerUIPlugin extends ViewPlugin { |
||||||
|
engine: Engine |
||||||
|
dispatch: React.Dispatch<any> = () => { } |
||||||
|
workspaceScriptRunnerDefaults: Record<string, string> |
||||||
|
customConfig: ScriptRunnerConfig |
||||||
|
configurations: ProjectConfiguration[] |
||||||
|
activeConfig: ProjectConfiguration |
||||||
|
enableCustomScriptRunner: boolean |
||||||
|
plugin: Plugin<any, CustomRemixApi> |
||||||
|
scriptRunnerProfileName: string |
||||||
|
constructor(engine: Engine) { |
||||||
|
super(profile) |
||||||
|
this.engine = engine |
||||||
|
this.workspaceScriptRunnerDefaults = {} |
||||||
|
this.plugin = this |
||||||
|
this.enableCustomScriptRunner = false // implement this later
|
||||||
|
} |
||||||
|
|
||||||
|
async onActivation() { |
||||||
|
|
||||||
|
this.on('filePanel', 'setWorkspace', async (workspace: string) => { |
||||||
|
this.activeConfig = null |
||||||
|
this.customConfig = |
||||||
|
{ |
||||||
|
defaultConfig: 'default', |
||||||
|
customConfig: { |
||||||
|
baseConfiguration: 'default', |
||||||
|
dependencies: [] |
||||||
|
} |
||||||
|
} |
||||||
|
await this.loadCustomConfig() |
||||||
|
await this.loadConfigurations() |
||||||
|
this.renderComponent() |
||||||
|
}) |
||||||
|
|
||||||
|
this.plugin.on('fileManager', 'fileSaved', async (file: string) => { |
||||||
|
|
||||||
|
if (file === configFileName && this.enableCustomScriptRunner) { |
||||||
|
await this.loadCustomConfig() |
||||||
|
this.renderComponent() |
||||||
|
} |
||||||
|
}) |
||||||
|
await this.loadCustomConfig() |
||||||
|
await this.loadConfigurations() |
||||||
|
this.renderComponent() |
||||||
|
} |
||||||
|
|
||||||
|
render() { |
||||||
|
return ( |
||||||
|
<div id="scriptrunnerTab"> |
||||||
|
<PluginViewWrapper plugin={this} /> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
setDispatch(dispatch: React.Dispatch<any>) { |
||||||
|
this.dispatch = dispatch |
||||||
|
this.renderComponent() |
||||||
|
} |
||||||
|
|
||||||
|
renderComponent() { |
||||||
|
this.dispatch({ |
||||||
|
customConfig: this.customConfig, |
||||||
|
configurations: this.configurations, |
||||||
|
activeConfig: this.activeConfig, |
||||||
|
enableCustomScriptRunner: this.enableCustomScriptRunner |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
updateComponent(state: IScriptRunnerState) { |
||||||
|
return ( |
||||||
|
<ScriptRunnerUI |
||||||
|
customConfig={state.customConfig} |
||||||
|
configurations={state.configurations} |
||||||
|
activeConfig={state.activeConfig} |
||||||
|
enableCustomScriptRunner={state.enableCustomScriptRunner} |
||||||
|
activateCustomScriptRunner={this.activateCustomScriptRunner.bind(this)} |
||||||
|
saveCustomConfig={this.saveCustomConfig.bind(this)} |
||||||
|
openCustomConfig={this.openCustomConfig.bind(this)} |
||||||
|
loadScriptRunner={this.selectScriptRunner.bind(this)} /> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
async selectScriptRunner(config: ProjectConfiguration) { |
||||||
|
if (await this.loadScriptRunner(config)) |
||||||
|
await this.saveCustomConfig(this.customConfig) |
||||||
|
} |
||||||
|
|
||||||
|
async loadScriptRunner(config: ProjectConfiguration): Promise<boolean> { |
||||||
|
|
||||||
|
const profile: Profile = await this.plugin.call('manager', 'getProfile', 'scriptRunner') |
||||||
|
this.scriptRunnerProfileName = profile.name |
||||||
|
const testPluginName = localStorage.getItem('test-plugin-name') |
||||||
|
const testPluginUrl = localStorage.getItem('test-plugin-url') |
||||||
|
|
||||||
|
let url = `${baseUrl}?template=${config.name}×tamp=${Date.now()}` |
||||||
|
if (testPluginName === 'scriptRunner') { |
||||||
|
// if testpluginurl has template specified only use that
|
||||||
|
if (testPluginUrl.indexOf('template') > -1) { |
||||||
|
url = testPluginUrl |
||||||
|
} else { |
||||||
|
baseUrl = `//${new URL(testPluginUrl).host}` |
||||||
|
url = `${baseUrl}?template=${config.name}×tamp=${Date.now()}` |
||||||
|
} |
||||||
|
} |
||||||
|
//console.log('loadScriptRunner', profile)
|
||||||
|
const newProfile: IframeProfile = { |
||||||
|
...profile, |
||||||
|
name: profile.name + config.name, |
||||||
|
location: 'hiddenPanel', |
||||||
|
url: url |
||||||
|
} |
||||||
|
|
||||||
|
let result = null |
||||||
|
try { |
||||||
|
this.setIsLoading(config.name, true) |
||||||
|
const plugin: IframePlugin = new IframePlugin(newProfile) |
||||||
|
if (!this.engine.isRegistered(newProfile.name)) { |
||||||
|
|
||||||
|
await this.engine.register(plugin) |
||||||
|
} |
||||||
|
await this.plugin.call('manager', 'activatePlugin', newProfile.name) |
||||||
|
|
||||||
|
this.activeConfig = config |
||||||
|
this.on(newProfile.name, 'log', this.log.bind(this)) |
||||||
|
this.on(newProfile.name, 'info', this.info.bind(this)) |
||||||
|
this.on(newProfile.name, 'warn', this.warn.bind(this)) |
||||||
|
this.on(newProfile.name, 'error', this.error.bind(this)) |
||||||
|
this.on(newProfile.name, 'dependencyError', this.dependencyError.bind(this)) |
||||||
|
this.customConfig.defaultConfig = config.name |
||||||
|
this.setErrorStatus(config.name, false, '') |
||||||
|
result = true |
||||||
|
} catch (e) { |
||||||
|
console.log('Error loading script runner: ', newProfile.name, e) |
||||||
|
const iframe = document.getElementById(`plugin-${newProfile.name}`); |
||||||
|
if (iframe) { |
||||||
|
await this.call('hiddenPanel', 'removeView', newProfile) |
||||||
|
} |
||||||
|
|
||||||
|
delete (this.engine as any).manager.profiles[newProfile.name] |
||||||
|
delete (this.engine as any).plugins[newProfile.name] |
||||||
|
console.log('Error loading script runner: ', newProfile.name, e) |
||||||
|
this.setErrorStatus(config.name, true, e) |
||||||
|
result = false |
||||||
|
} |
||||||
|
|
||||||
|
this.setIsLoading(config.name, false) |
||||||
|
this.renderComponent() |
||||||
|
return result |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
async execute(script: string, filePath: string) { |
||||||
|
this.call('terminal', 'log', { value: `running ${filePath} ...`, type: 'info' }) |
||||||
|
if (!this.scriptRunnerProfileName || !this.engine.isRegistered(`${this.scriptRunnerProfileName}${this.activeConfig.name}`)) { |
||||||
|
if (!await this.loadScriptRunner(this.activeConfig)) { |
||||||
|
console.error('Error loading script runner') |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
try { |
||||||
|
this.setIsLoading(this.activeConfig.name, true) |
||||||
|
await this.call(`${this.scriptRunnerProfileName}${this.activeConfig.name}`, 'execute', script, filePath) |
||||||
|
} catch (e) { |
||||||
|
console.error('Error executing script', e) |
||||||
|
} |
||||||
|
this.setIsLoading(this.activeConfig.name, false) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
async setErrorStatus(name: string, status: boolean, error: string) { |
||||||
|
this.configurations.forEach((config) => { |
||||||
|
if (config.name === name) { |
||||||
|
config.errorStatus = status |
||||||
|
config.error = error |
||||||
|
} |
||||||
|
}) |
||||||
|
this.renderComponent() |
||||||
|
} |
||||||
|
|
||||||
|
async setIsLoading(name: string, status: boolean) { |
||||||
|
if (status) { |
||||||
|
this.emit('statusChanged', { |
||||||
|
key: 'loading', |
||||||
|
type: 'info', |
||||||
|
title: 'loading...' |
||||||
|
}) |
||||||
|
} else { |
||||||
|
this.emit('statusChanged', { |
||||||
|
key: 'none' |
||||||
|
}) |
||||||
|
} |
||||||
|
this.configurations.forEach((config) => { |
||||||
|
if (config.name === name) { |
||||||
|
config.isLoading = status |
||||||
|
} |
||||||
|
}) |
||||||
|
this.renderComponent() |
||||||
|
} |
||||||
|
|
||||||
|
async dependencyError(data: any) { |
||||||
|
console.log('Script runner dependency error: ', data) |
||||||
|
let message = `Error loading dependencies: ` |
||||||
|
if (isArray(data.data)) { |
||||||
|
data.data.forEach((data: any) => { |
||||||
|
message += `${data}` |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const modal: AppModal = { |
||||||
|
id: 'TemplatesSelection', |
||||||
|
title: 'Missing dependencies', |
||||||
|
message: `${message} \n\n You may need to setup a script engine for this workspace to load the correct dependencies. Do you want go to setup now?`, |
||||||
|
okLabel: window._intl.formatMessage({ id: 'filePanel.ok' }), |
||||||
|
cancelLabel: 'ignore' |
||||||
|
} |
||||||
|
const modalResult = await this.plugin.call('notification' as any, 'modal', modal) |
||||||
|
if (modalResult) { |
||||||
|
await this.plugin.call('menuicons', 'select', 'scriptRunnerBridge') |
||||||
|
} else { |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async log(data: any) { |
||||||
|
this.emit('log', data) |
||||||
|
} |
||||||
|
|
||||||
|
async warn(data: any) { |
||||||
|
this.emit('warn', data) |
||||||
|
} |
||||||
|
|
||||||
|
async error(data: any) { |
||||||
|
this.emit('error', data) |
||||||
|
} |
||||||
|
|
||||||
|
async info(data: any) { |
||||||
|
this.emit('info', data) |
||||||
|
} |
||||||
|
|
||||||
|
async loadCustomConfig(): Promise<void> { |
||||||
|
try { |
||||||
|
const content = await this.plugin.call('fileManager', 'readFile', configFileName) |
||||||
|
const parsed = JSON.parse(content) |
||||||
|
this.customConfig = parsed |
||||||
|
} catch (e) { |
||||||
|
this.customConfig = { |
||||||
|
defaultConfig: 'default', |
||||||
|
customConfig: { |
||||||
|
baseConfiguration: 'default', |
||||||
|
dependencies: [] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
async openCustomConfig() { |
||||||
|
try { |
||||||
|
await this.plugin.call('fileManager', 'open', '.remix/script.config.json') |
||||||
|
} catch (e) { |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async loadConfigurations() { |
||||||
|
try { |
||||||
|
const response = await axios.get(`${baseUrl}/projects.json?timestamp=${Date.now()}`); |
||||||
|
this.configurations = response.data; |
||||||
|
// find the default otherwise pick the first one as the active
|
||||||
|
this.configurations.forEach((config) => { |
||||||
|
if (config.name === (this.customConfig.defaultConfig)) { |
||||||
|
this.activeConfig = config; |
||||||
|
} |
||||||
|
}); |
||||||
|
if (!this.activeConfig) { |
||||||
|
this.activeConfig = this.configurations[0]; |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.error("Error fetching the projects data:", error); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
async saveCustomConfig(content: ScriptRunnerConfig) { |
||||||
|
if (content.customConfig.dependencies.length === 0 && content.defaultConfig === 'default') { |
||||||
|
try { |
||||||
|
const exists = await this.plugin.call('fileManager', 'exists', '.remix/script.config.json') |
||||||
|
if (exists) { |
||||||
|
await this.plugin.call('fileManager', 'remove', '.remix/script.config.json') |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
await this.plugin.call('fileManager', 'writeFile', '.remix/script.config.json', JSON.stringify(content, null, 2)) |
||||||
|
} |
||||||
|
|
||||||
|
async activateCustomScriptRunner(config: customScriptRunnerConfig) { |
||||||
|
try { |
||||||
|
const result = await axios.post(customBuildUrl, config) |
||||||
|
if (result.data.hash) { |
||||||
|
|
||||||
|
const newConfig: ProjectConfiguration = { |
||||||
|
name: result.data.hash, |
||||||
|
title: 'Custom configuration', |
||||||
|
publish: true, |
||||||
|
description: `Extension of ${config.baseConfiguration}`, |
||||||
|
dependencies: config.dependencies, |
||||||
|
replacements: {}, |
||||||
|
errorStatus: false, |
||||||
|
error: '', |
||||||
|
isLoading: false |
||||||
|
}; |
||||||
|
this.configurations.push(newConfig) |
||||||
|
this.renderComponent() |
||||||
|
await this.loadScriptRunner(result.data.hash) |
||||||
|
} |
||||||
|
return result.data.hash |
||||||
|
} catch (error) { |
||||||
|
let message |
||||||
|
if (error.response) { |
||||||
|
// The request was made and the server responded with a status code
|
||||||
|
// that falls out of the range of 2xx
|
||||||
|
console.log('Error status:', error.response.status); |
||||||
|
console.log('Error data:', error.response.data); // This should give you the output being sent
|
||||||
|
console.log('Error headers:', error.response.headers); |
||||||
|
|
||||||
|
if (error.response.data.error) { |
||||||
|
|
||||||
|
if (isArray(error.response.data.error)) { |
||||||
|
const message = `${error.response.data.error[0]}` |
||||||
|
this.plugin.call('notification', 'alert', { |
||||||
|
id: 'scriptalert', |
||||||
|
message, |
||||||
|
title: 'Error' |
||||||
|
}) |
||||||
|
throw new Error(message) |
||||||
|
} |
||||||
|
message = `${error.response.data.error}` |
||||||
|
} |
||||||
|
message = `Uknown error: ${error.response.data}` |
||||||
|
this.plugin.call('notification', 'alert', { |
||||||
|
id: 'scriptalert', |
||||||
|
message, |
||||||
|
title: 'Error' |
||||||
|
}) |
||||||
|
throw new Error(message) |
||||||
|
} else if (error.request) { |
||||||
|
// The request was made but no response was received
|
||||||
|
console.log('No response received:', error.request); |
||||||
|
throw new Error('No response received') |
||||||
|
} else { |
||||||
|
// Something happened in setting up the request that triggered an Error
|
||||||
|
console.log('Error message:', error.message); |
||||||
|
throw new Error(error.message) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,14 @@ |
|||||||
|
import { IFilePanel } from '@remixproject/plugin-api' |
||||||
|
import { Profile, StatusEvents } from '@remixproject/plugin-utils' |
||||||
|
|
||||||
|
export interface IMenuIconsApi { |
||||||
|
events: { |
||||||
|
toggleContent: (name: string) => void, |
||||||
|
showContent: (name: string) => void |
||||||
|
} & StatusEvents |
||||||
|
methods: { |
||||||
|
select: (name: string) => void |
||||||
|
linkContent: (profile: Profile) => void |
||||||
|
unlinkContent: (profile: Profile) => void |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,2 @@ |
|||||||
|
export { ScriptRunnerUI } from './lib/script-runner-ui'; |
||||||
|
export * from './types'; |
@ -0,0 +1,168 @@ |
|||||||
|
import React, { useEffect, useState } from "react"; |
||||||
|
import { customScriptRunnerConfig, Dependency, ProjectConfiguration } from "../types"; |
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; |
||||||
|
import { faToggleOff, faToggleOn, faTrash } from "@fortawesome/free-solid-svg-icons"; |
||||||
|
import { CustomTooltip } from "@remix-ui/helper"; |
||||||
|
|
||||||
|
export interface ScriptRunnerUIProps { |
||||||
|
publishedConfigurations: ProjectConfiguration[]; |
||||||
|
openCustomConfig: () => any; |
||||||
|
saveCustomConfig(content: customScriptRunnerConfig): void; |
||||||
|
activateCustomScriptRunner(config: customScriptRunnerConfig): Promise<string>; |
||||||
|
customConfig: customScriptRunnerConfig; |
||||||
|
} |
||||||
|
|
||||||
|
export const CustomScriptRunner = (props: ScriptRunnerUIProps) => { |
||||||
|
const [dependencies, setDependencies] = useState<Dependency[]>([]); |
||||||
|
const [name, setName] = useState<string>(''); |
||||||
|
const [alias, setAlias] = useState<string>(''); |
||||||
|
const [version, setVersion] = useState<string>(''); |
||||||
|
const [baseConfig, setBaseConfig] = useState<string>('default'); |
||||||
|
const [loading, setLoading] = useState<boolean>(false); |
||||||
|
const [useRequire, setUseRequire] = useState<boolean>(false) |
||||||
|
|
||||||
|
const { customConfig } = props; |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!customConfig) return; |
||||||
|
setDependencies(customConfig.dependencies); |
||||||
|
setBaseConfig(customConfig.baseConfiguration); |
||||||
|
},[customConfig]) |
||||||
|
|
||||||
|
const handleAddDependency = () => { |
||||||
|
if (name.trim() && version.trim()) { |
||||||
|
const newDependency: Dependency = { name, version, require: useRequire, alias }; |
||||||
|
setDependencies([...dependencies, newDependency]); |
||||||
|
setName(''); |
||||||
|
setVersion(''); |
||||||
|
} else { |
||||||
|
alert('Please fill out both name and version.'); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const handleRemoveDependency = (index: number) => { |
||||||
|
const updatedDependencies = dependencies.filter((_, i) => i !== index); |
||||||
|
setDependencies(updatedDependencies); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleSaveToFile = () => { |
||||||
|
const fileData = JSON.stringify(dependencies, null, 2); |
||||||
|
console.log(fileData, baseConfig); |
||||||
|
const customConfig: customScriptRunnerConfig = { baseConfiguration: baseConfig, dependencies }; |
||||||
|
console.log(customConfig); |
||||||
|
props.saveCustomConfig(customConfig); |
||||||
|
}; |
||||||
|
|
||||||
|
const openConfig = async () => { |
||||||
|
const fileData: customScriptRunnerConfig = await props.openCustomConfig(); |
||||||
|
} |
||||||
|
|
||||||
|
const activateCustomConfig = async () => { |
||||||
|
const customConfig: customScriptRunnerConfig = { baseConfiguration: baseConfig, dependencies }; |
||||||
|
setLoading(true); |
||||||
|
try { |
||||||
|
await props.activateCustomScriptRunner(customConfig); |
||||||
|
} catch (e) { |
||||||
|
console.log(e) |
||||||
|
} finally { |
||||||
|
setLoading(false); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const onSelectBaseConfig = (e: React.ChangeEvent<HTMLSelectElement>) => { |
||||||
|
setBaseConfig(e.target.value); |
||||||
|
} |
||||||
|
|
||||||
|
const toggleRequire = () => { |
||||||
|
setUseRequire((prev) => !prev) |
||||||
|
} |
||||||
|
|
||||||
|
if (loading) { |
||||||
|
return <div style={{ padding: '20px', maxWidth: '400px', margin: 'auto' }}> |
||||||
|
<div className="text-center py-5"> |
||||||
|
<i className="fas fa-spinner fa-pulse fa-2x"></i> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div style={{ padding: '20px', maxWidth: '400px', margin: 'auto' }}> |
||||||
|
<h5>Custom configuration</h5> |
||||||
|
<label>Select a base configuration</label> |
||||||
|
<select value={baseConfig} className="form-control" onChange={onSelectBaseConfig} style={{ marginBottom: '10px' }}> |
||||||
|
<option value="none">Select a base configuration</option> |
||||||
|
{props.publishedConfigurations.map((config: ProjectConfiguration, index) => ( |
||||||
|
<option key={index} value={config.name}> |
||||||
|
{config.name} |
||||||
|
</option> |
||||||
|
))} |
||||||
|
</select> |
||||||
|
<label>Add dependencies</label> |
||||||
|
<div style={{ marginBottom: '10px' }}> |
||||||
|
<input |
||||||
|
type="text" |
||||||
|
placeholder="Dependency Name" |
||||||
|
value={name} |
||||||
|
className="form-control" |
||||||
|
onChange={(e) => setName(e.target.value)} |
||||||
|
style={{ marginRight: '10px' }} |
||||||
|
/> |
||||||
|
<input |
||||||
|
type="text" |
||||||
|
placeholder="Alias" |
||||||
|
className="form-control mt-1" |
||||||
|
value={alias} |
||||||
|
onChange={(e) => setAlias(e.target.value)} /> |
||||||
|
<input |
||||||
|
type="text" |
||||||
|
placeholder="Version" |
||||||
|
className="form-control mt-1" |
||||||
|
value={version} |
||||||
|
onChange={(e) => setVersion(e.target.value)} |
||||||
|
/> |
||||||
|
<CustomTooltip |
||||||
|
placement="bottom" |
||||||
|
tooltipText="use require when the module doesn't support import statements" |
||||||
|
> |
||||||
|
<div> |
||||||
|
<label className="pr-2 pt-2">Use 'require':</label> |
||||||
|
<FontAwesomeIcon className={useRequire ? 'text-success' : ''} onClick={toggleRequire} icon={useRequire ? faToggleOn : faToggleOff}></FontAwesomeIcon> |
||||||
|
</div> |
||||||
|
</CustomTooltip> |
||||||
|
<button |
||||||
|
className="btn btn-primary w-100 mt-1" |
||||||
|
onClick={handleAddDependency}> |
||||||
|
Add |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<ul> |
||||||
|
{dependencies.map((dependency, index) => ( |
||||||
|
<li key={index} style={{ marginBottom: '5px' }}> |
||||||
|
<div className="d-flex align-items-baseline justify-content-between"> |
||||||
|
{dependency.name} - {dependency.version} |
||||||
|
<button |
||||||
|
onClick={() => handleRemoveDependency(index)} |
||||||
|
className="btn btn-danger" |
||||||
|
style={{ marginLeft: '10px' }} |
||||||
|
> |
||||||
|
<FontAwesomeIcon icon={faTrash} /> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
))} |
||||||
|
</ul> |
||||||
|
{dependencies.length > 0 && ( |
||||||
|
<button className="btn btn-primary w-100" onClick={handleSaveToFile} style={{ marginTop: '20px' }}> |
||||||
|
Save config |
||||||
|
</button> |
||||||
|
)} |
||||||
|
<button className="btn btn-primary w-100" onClick={openConfig} style={{ marginTop: '20px' }}> |
||||||
|
Open config |
||||||
|
</button> |
||||||
|
{dependencies.length > 0 && ( |
||||||
|
<button className="btn btn-success w-100" onClick={activateCustomConfig} style={{ marginTop: '20px' }}> |
||||||
|
Activate |
||||||
|
</button>)} |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,103 @@ |
|||||||
|
import React, { useEffect, useState } from "react"; |
||||||
|
import { Accordion, Button } from "react-bootstrap"; |
||||||
|
import { customScriptRunnerConfig, ProjectConfiguration } from "../types"; |
||||||
|
import { faCaretDown, faCaretRight, faCheck, faExclamationCircle, faRedoAlt, faToggleOn } from "@fortawesome/free-solid-svg-icons"; |
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; |
||||||
|
import { CustomScriptRunner } from "./custom-script-runner"; |
||||||
|
import { CustomTooltip } from "@remix-ui/helper"; |
||||||
|
|
||||||
|
export interface ScriptRunnerUIProps { |
||||||
|
loadScriptRunner: (config: ProjectConfiguration) => void; |
||||||
|
openCustomConfig: () => any; |
||||||
|
saveCustomConfig(content: customScriptRunnerConfig): void; |
||||||
|
activateCustomScriptRunner(config: customScriptRunnerConfig): Promise<string>; |
||||||
|
customConfig: customScriptRunnerConfig; |
||||||
|
configurations: ProjectConfiguration[]; |
||||||
|
activeConfig: ProjectConfiguration; |
||||||
|
enableCustomScriptRunner: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export const ScriptRunnerUI = (props: ScriptRunnerUIProps) => { |
||||||
|
const { loadScriptRunner, configurations, activeConfig, enableCustomScriptRunner } = props; |
||||||
|
const [activeKey, setActiveKey] = useState('default'); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (activeConfig) { |
||||||
|
setActiveKey(activeConfig.name) |
||||||
|
} |
||||||
|
},[activeConfig]) |
||||||
|
|
||||||
|
if (!configurations) { |
||||||
|
return <div>Loading...</div>; |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="px-1"> |
||||||
|
<Accordion activeKey={activeKey} defaultActiveKey="default"> |
||||||
|
{configurations.filter((config) => config.publish).map((config: ProjectConfiguration, index) => ( |
||||||
|
<div key={index}> |
||||||
|
<div className="d-flex align-items-baseline justify-content-between"> |
||||||
|
<Accordion.Toggle as={Button} variant="link" eventKey={config.name} |
||||||
|
style={{ |
||||||
|
overflowX: 'hidden', |
||||||
|
textOverflow: 'ellipsis' |
||||||
|
}} |
||||||
|
onClick={() => setActiveKey(activeKey === config.name ? '' : config.name)} |
||||||
|
> |
||||||
|
<div className="d-flex"> |
||||||
|
{activeKey === config.name ? |
||||||
|
<FontAwesomeIcon icon={faCaretDown}></FontAwesomeIcon> : |
||||||
|
<FontAwesomeIcon icon={faCaretRight}></FontAwesomeIcon>} |
||||||
|
<div data-id={`sr-list-${config.name}`} className="pl-2">{config.title || config.name}</div> |
||||||
|
</div> |
||||||
|
</Accordion.Toggle> |
||||||
|
<div className="d-flex align-items-baseline"> |
||||||
|
{config.isLoading && <div className=""> |
||||||
|
<i className="fas fa-spinner fa-spin mr-1"></i> |
||||||
|
</div>} |
||||||
|
{config.errorStatus && config.error && <div className="text-danger"> |
||||||
|
<CustomTooltip tooltipText={config.error}> |
||||||
|
<FontAwesomeIcon data-id={`sr-error-${config.name}`} icon={faExclamationCircle}></FontAwesomeIcon> |
||||||
|
</CustomTooltip> |
||||||
|
|
||||||
|
</div>} |
||||||
|
{!config.isLoading && config.errorStatus && config.error && |
||||||
|
<div onClick={() => loadScriptRunner(config)} className="pointer px-2"> |
||||||
|
<FontAwesomeIcon data-id={`sr-reload-${config.name}`} icon={faRedoAlt}></FontAwesomeIcon> |
||||||
|
</div>} |
||||||
|
{!config.isLoading && !config.errorStatus && !config.error && |
||||||
|
<div onClick={() => loadScriptRunner(config)} className="pointer px-2"> |
||||||
|
{activeConfig && activeConfig.name !== config.name ? |
||||||
|
<FontAwesomeIcon data-id={`sr-toggle-${config.name}`} icon={faToggleOn}></FontAwesomeIcon> : |
||||||
|
<FontAwesomeIcon data-id={`sr-loaded-${config.name}`} className="text-success" icon={faCheck}></FontAwesomeIcon> |
||||||
|
} |
||||||
|
</div> |
||||||
|
} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<Accordion.Collapse className="px-4" eventKey={config.name}> |
||||||
|
<> |
||||||
|
<p><strong>Description: </strong>{config.description}</p> |
||||||
|
<p><strong>Dependencies:</strong></p> |
||||||
|
<ul> |
||||||
|
{config.dependencies.map((dep, depIndex) => ( |
||||||
|
<li data-id={`dependency-${dep.name}-${dep.version}`} key={depIndex}> |
||||||
|
<strong>{dep.name}</strong> (v{dep.version}) |
||||||
|
</li> |
||||||
|
))} |
||||||
|
</ul></> |
||||||
|
</Accordion.Collapse></div>))} |
||||||
|
</Accordion> |
||||||
|
{enableCustomScriptRunner && |
||||||
|
<CustomScriptRunner |
||||||
|
customConfig={props.customConfig} |
||||||
|
activateCustomScriptRunner={props.activateCustomScriptRunner} |
||||||
|
saveCustomConfig={props.saveCustomConfig} |
||||||
|
openCustomConfig={props.openCustomConfig} |
||||||
|
publishedConfigurations={configurations.filter((config) => config.publish)} |
||||||
|
/>} |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
@ -0,0 +1,37 @@ |
|||||||
|
import { defaultConfig } from "@web3modal/ethers5/react"; |
||||||
|
|
||||||
|
export interface Dependency { |
||||||
|
version: string; |
||||||
|
name: string; |
||||||
|
alias?: string; |
||||||
|
import?: boolean; |
||||||
|
require: boolean; |
||||||
|
windowImport?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export interface Replacements { |
||||||
|
[key: string]: string; |
||||||
|
} |
||||||
|
|
||||||
|
export interface ProjectConfiguration { |
||||||
|
name: string; |
||||||
|
publish: boolean; |
||||||
|
description: string; |
||||||
|
dependencies: Dependency[]; |
||||||
|
replacements: Replacements; |
||||||
|
title: string; |
||||||
|
errorStatus: boolean; |
||||||
|
error: string; |
||||||
|
isLoading: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export interface customScriptRunnerConfig { |
||||||
|
baseConfiguration: string; |
||||||
|
dependencies: Dependency[]; |
||||||
|
} |
||||||
|
|
||||||
|
export interface ScriptRunnerConfig { |
||||||
|
defaultConfig: string, |
||||||
|
customConfig: customScriptRunnerConfig |
||||||
|
} |
||||||
|
|
Loading…
Reference in new issue