diff --git a/e2e/cypress.json b/e2e/cypress.json index 0967ef42..17ef242e 100644 --- a/e2e/cypress.json +++ b/e2e/cypress.json @@ -1 +1,3 @@ -{} +{ + "baseUrl": "http://localhost:3000" +} diff --git a/e2e/cypress/helpers.ts b/e2e/cypress/helpers.ts index 587b2932..607aa11a 100644 --- a/e2e/cypress/helpers.ts +++ b/e2e/cypress/helpers.ts @@ -1,13 +1,227 @@ import Web3 = require("web3"); import * as CrowdFundFactory from "../../contract/build/contracts/CrowdFundFactory.json"; +import { WebsocketProvider } from "web3/providers"; +import * as sigUtil from "eth-sig-util"; -export const loadWeb3 = (window: any) => { - window.web3 = new Web3(`ws://localhost:8545`); - return window.web3.eth.net.getId().then((id: string) => { - console.log("loadWeb3: connected to networkId: " + id); +const WEB3_PROVIDER = `ws://localhost:8545`; + +// accounts generated using `ganache -s testGrantIo` +export const testAccounts = [ + [ + "0x0a4ab0753416ce1bebef3855fd081c170ae0194f", + "872017489e13bc3d7fec343d14691ac3c95a7904651113ce018a6ee21ae70a6e" + ], + [ + "0x926604d5a2383eae9d88eb4e884b1a34b3546194", + "492e6984a9faa04e5aad31219db4db9c927ed011b2b56b3068b1ff2b34e43f00" + ], + [ + "0xf277b4a0b9c89c07cc331b38d87a5a382501ed1a", + "c6aba74cc839af98def2819a85949847f80af42d11fefab4ecb713752261099a" + ], + [ + "0x6e6324d0927fb0aee2cbb1c915bcdc47c4f45a37", + "c66b30565895ef84c9e1cda943d828f9a91e6a0c0624caae76a661e35c2dc722" + ], + [ + "0xa696d8f7cfd0136c971492d0e9312c139cb18173", + "5f33d8df218aaf0b2bfa640d04f69aa234a1997fa08b4db606e6b022aa18cc8c" + ], + [ + "0xf90685b1a48259e192876744ff7829c1ba995093", + "9d1e415439a8411e354e72acd944cae82c3c4296c5bef6ef5d848a97a94f9ea8" + ], + [ + "0x9a4e6001044e16c5fe1c98f1ee71ce0b0005ad9b", + "cb9c060f86cec728b11a6d1143de09d221bc3a417ea9ac3e3b2922589c259ed5" + ], + [ + "0x3d03d67dbb26d13fafeebba55f5b6346213f602a", + "68e1c39abdcc8fc7d801237eb730b8e0f3199e1841209944f8c63580749a6f61" + ], + [ + "0x78ae7d98a2291093a4ff9e5003de4ab0d2a82169", + "f4c9cf60f3f91559af004f7a8de8dfebe3121a2ac418ea4f00ec34b5c841ed42" + ], + [ + "0x3cdbcc74770a13cba2045da4740c4c91ddd99b9e", + "d5c02d0db53291c4074a249e983a2f117d3ebd4155d606edde51a9be7e5deee6" + ] +]; + +// keep our own web3 to avoid messing with App's Provider +let e2eWeb3: Web3; +let appWeb3: Web3; +// open(method: string, url: string, async: boolean, username?: string | null, password?: string | null): void; +export const loadWeb3 = (swapAcctIndex: number) => (window: any) => { + if (appWeb3) { + ((appWeb3.currentProvider as WebsocketProvider) + .connection as WebSocket).close(); + } + if (e2eWeb3) { + ((e2eWeb3.currentProvider as WebsocketProvider) + .connection as WebSocket).close(); + } + appWeb3 = new Web3(WEB3_PROVIDER); + e2eWeb3 = new Web3(WEB3_PROVIDER); + + // modify backend proposal fields derived using server time + modifyProposalsApi(window); + + // window.web3.provider gets passed into new Web3 by frontend + // we will inject this for setting contract.options.gas in frontend + // this solves gas exhaustion issues + (appWeb3.currentProvider as any)._e2eContractGas = 5000000; + + // intercept eth_signTypedData_v3 calls and sign (ganache doesn't support yet) + // intercept eth_accounts RPC calls and fiddle with account[0] + const origSend = appWeb3.currentProvider.send; + const newSend = makeSendSpy(origSend, swapAcctIndex); + appWeb3.currentProvider.send = newSend; + window.web3 = appWeb3; +}; + +// intercept and modify/replace json rpc calls +const makeSendSpy = (origSend: any, swapAcctIndex: number) => { + return function(opts: any, cb: any) { + let interceptedCb = cb; + if (opts.method === "eth_signTypedData_v3") { + const [acct, data] = opts.params; + console.log(`e2e: eth_signTypedData_v3 signing for ${acct}`); + const rawTypedData = JSON.parse(data); + const signedMessage = signTypedData(acct, rawTypedData); + interceptedCb(null, { result: signedMessage }); + } else { + if (opts.method === "eth_accounts") { + interceptedCb = (err: any, res: any) => { + console.log( + `e2e: swapping account[0] with account[${swapAcctIndex}] (${ + res.result[swapAcctIndex] + })` + ); + const acctZero = res.result[0]; + res.result[0] = res.result[swapAcctIndex]; + res.result[swapAcctIndex] = acctZero; + cb(err, res); + }; + } + origSend.bind(appWeb3.currentProvider)(opts, interceptedCb); + } + }; +}; + +// sign data using ganache determined private keys + eth-sig-util +const signTypedData = (account: string, typedData: any) => { + const testAccount = testAccounts.find(ta => ta[0] === account.toLowerCase()); + if (testAccount) { + const privateKey = new Buffer(testAccount[1], "hex"); + const sig = sigUtil.signTypedData(privateKey, { data: typedData }); + return sig; + } else { + throw new Error( + `e2e helpers.sign: Could not find privateKey for account ${account}` + ); + } +}; + +// cypress doesn't yet support modifying incoming xhr requests +// https://github.com/cypress-io/cypress/tree/full-network-stubbing-687 +const modifyProposalsApi = (window: any) => { + patchXMLHttpRequest(window, /proposals\/([0-9]+)?/, text => { + const json = JSON.parse(text); + const mod = (proposal: any) => { + if (proposal.crowdFund) { + proposal.crowdFund.milestones.forEach((ms: any) => { + if (ms.state === "ACTIVE") { + // ms.state based on server time, let's use browser time + ms.state = + (ms.payoutRequestVoteDeadline > window.Date.now() && "ACTIVE") || + (ms.percentAgainstPayout >= 50 && "REJECTED") || + "PAID"; + if (ms.state != "ACTIVE") { + console.log( + `e2e modifyProposalApi changed ms.state to ${ms.state}` + ); + } + } + return ms; + }); + } + return proposal; + }; + if (Array.isArray(json)) { + json.forEach(mod); + } else { + mod(json); + } + return JSON.stringify(json); }); }; +// patch the window's XMLHttpRequest with modify fn +const patchXMLHttpRequest = ( + window: any, + urlMatch: RegExp, + modify: (text: string) => string +) => { + const script = window.document.createElement("script"); + script.text = xhookText; + window.__afterxhook__ = function() { + console.log("xhook.after declaration"); + window.xhook.after(function(request: any, response: any) { + if (urlMatch.test(request.url)) response.text = modify(response.text); + }); + }; + window.document.head.appendChild(script); +}; + +const mineBlock = () => + e2eWeb3.currentProvider.send( + { + jsonrpc: "2.0", + method: "evm_mine", + params: [], + id: Date.now() + }, + () => null + ); + +const evmIncreaseTime = (seconds: number) => + new Promise((res, rej) => { + e2eWeb3.currentProvider.send( + { + jsonrpc: "2.0", + method: "evm_increaseTime", + params: [seconds], + id: Date.now() + }, + (err, _) => (err && rej(err)) || res() + ); + }); + +export const increaseTime = (cy: Cypress.Chainable, ms: number) => { + console.log("increasetime", ms); + cy.log("INCREASE TIME", ms + "ms"); + cy.window({ log: false }) + .then(w => evmIncreaseTime(Math.round(ms / 1000))) + .then(() => syncTimeWithEvm(cy)); +}; + +export const syncTimeWithEvm = (cy: Cypress.Chainable) => { + cy.window({ log: false }) + .then(w => { + mineBlock(); + return e2eWeb3.eth + .getBlock("latest") + .then((x: any) => x.timestamp * 1000); + }) + .then(t => { + cy.log("SYNC TIME WITH EVM", new Date(t).toString()); + cy.clock({ log: false }).then(x => x.restore()); // important for repeated calls! + cy.clock(t, ["Date"] as any, { log: false }); + }); +}; + export const createCrowdFund = (web3: Web3) => { const HOUR = 3600; const DAY = HOUR * 24; @@ -41,9 +255,9 @@ export const createCrowdFund = (web3: Web3) => { immediateFirstMilestonePayout ) .send({ - from: accounts[4], + from: accounts[4] // important - gas: 3695268 + // gas: 3695268 }) .then( (receipt: any) => @@ -70,3 +284,9 @@ export const randomHex = function(len: number) { } return r; }; + +// XHook - v1.4.9 - https://github.com/jpillora/xhook +// Jaime Pillora - MIT Copyright 2018 +const xhookText = + '(function(a,b){var c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H=[].indexOf||function(a){for(var b=0,c=this.length;b=0||(f=f===b?d[a].length:f,d[a].splice(f,0,c))},c[m]=function(a,c){var f;if(a===b)return void(d={});c===b&&(d[a]=[]),f=e(a).indexOf(c),f!==-1&&e(a).splice(f,1)},c[h]=function(){var b,d,f,g,h,i,j,k;for(b=D(arguments),d=b.shift(),a||(b[0]=z(b[0],y(d))),g=c["on"+d],g&&g.apply(c,b),k=e(d).concat(e("*")),f=i=0,j=k.length;i2)throw"invalid hook";return F[n](d,a,b)},F[c]=function(a,b){if(a.length<2||a.length>3)throw"invalid hook";return F[n](c,a,b)},F.enable=function(){q[u]=t,"function"==typeof r&&(q[g]=r),k&&(q[i]=s)},F.disable=function(){q[u]=F[u],q[g]=F[g],k&&(q[i]=k)},v=F.headers=function(a,b){var c,d,e,f,g,h,i,j,k;switch(null==b&&(b={}),typeof a){case"object":d=[];for(e in a)g=a[e],f=e.toLowerCase(),d.push(f+":\\t"+g);return d.join("\\n")+"\\n";case"string":for(d=a.split("\\n"),i=0,j=d.length;ib&&b<4;)k[o]=++b,1===b&&k[h]("loadstart",{}),2===b&&G(),4===b&&(G(),E()),k[h]("readystatechange",{}),4===b&&(t.async===!1?g():setTimeout(g,0))},g=function(){l||k[h]("load",{}),k[h]("loadend",{}),l&&(k[o]=0)},b=0,x=function(a){var b,d;if(4!==a)return void i(a);b=F.listeners(c),(d=function(){var a;return b.length?(a=b.shift(),2===a.length?(a(t,w),d()):3===a.length&&t.async?a(t,w,d):d()):i(4)})()},k=t.xhr=f(),I.onreadystatechange=function(a){try{2===I[o]&&r()}catch(a){}4===I[o]&&(D=!1,r(),q()),x(I[o])},m=function(){l=!0},k[n]("error",m),k[n]("timeout",m),k[n]("abort",m),k[n]("progress",function(){b<3?x(3):k[h]("readystatechange",{})}),("withCredentials"in I||F.addWithCredentials)&&(k.withCredentials=!1),k.status=0,L=e.concat(p);for(J=0,K=L.length;J +import { + loadWeb3, + increaseTime, + syncTimeWithEvm, + randomString +} from "../helpers"; +import { createDemoProposal, authenticateUser } from "../parts"; + +describe("authenticate", () => { + it("authenticates and creates if necessary", () => { + authenticateUser(cy, 0); + }); +}); diff --git a/e2e/cypress/integration/browse.spec.ts b/e2e/cypress/integration/browse.spec.ts index 237ceac2..ab3c5a94 100644 --- a/e2e/cypress/integration/browse.spec.ts +++ b/e2e/cypress/integration/browse.spec.ts @@ -3,13 +3,13 @@ import { loadWeb3, randomString, randomHex } from "../helpers"; describe("browse", () => { it("should load and be able to browse pages", () => { - cy.visit("http://localhost:3000", { onBeforeLoad: loadWeb3 }); + cy.visit("http://localhost:3000", { onBeforeLoad: loadWeb3(0) }); cy.title().should("include", "Grant.io - Home"); // test hero create link cy.get('.Home-hero-buttons a[href="/create"]') // {force: true} here overcomes a strange issue where the button moves up under the header - // this is likely a cypress-related problem + // this is likely a cypress scroll related problem .click({ force: true }); cy.title().should("include", "Grant.io - Create a Proposal"); diff --git a/e2e/cypress/integration/create.cancel.spec.ts b/e2e/cypress/integration/create.cancel.spec.ts new file mode 100644 index 00000000..b1c682af --- /dev/null +++ b/e2e/cypress/integration/create.cancel.spec.ts @@ -0,0 +1,47 @@ +/// +import { + loadWeb3, + increaseTime, + syncTimeWithEvm, + randomString +} from "../helpers"; +import { createDemoProposal, authenticateUser } from "../parts"; + +describe("proposal", () => { + const id = randomString(); + const title = `[${id}] e2e create cancel`; + const amount = "1"; + + afterEach(function() { + if (this.currentTest.state === "failed") { + (Cypress as any).runner.stop(); + } + }); + + it("authenticates and creates if necessary", () => { + authenticateUser(cy, 0); + }); + + it("creates demo proposal", () => { + createDemoProposal(cy, title, amount); + }); + + it("cancels the proposal", () => { + cy.contains(".Proposal-top-main-menu > .ant-btn", "Actions").click(); + cy.contains(".ant-dropdown-menu-item", "Cancel proposal").click(); + cy.contains(".ant-modal-footer > div button", "Confirm").click(); + cy.contains("body", "Proposal didn’t get funded", { timeout: 20000 }); + cy.get(".ant-modal-wrap").should("not.be.visible"); + cy.contains(".Proposal-top-main-menu > .ant-btn", "Actions").click(); + cy.contains(".ant-dropdown-menu-item", "Cancel proposal").should( + "have.attr", + "aria-disabled", + "true" + ); + }); + + it("should appear unfunded to outsiders (account 9)", () => { + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(9) })); + cy.contains("body", "Proposal didn’t get funded", { timeout: 20000 }); + }); +}); diff --git a/e2e/cypress/integration/create.flow.spec.ts b/e2e/cypress/integration/create.flow.spec.ts new file mode 100644 index 00000000..bb8f493a --- /dev/null +++ b/e2e/cypress/integration/create.flow.spec.ts @@ -0,0 +1,173 @@ +/// +import { loadWeb3, randomString, randomHex, syncTimeWithEvm } from "../helpers"; +import { authenticateUser } from "../parts"; + +describe("create.flow", () => { + const time = new Date().toLocaleString(); + const id = randomString(); + const randomEthHex = randomHex(32); + const nextYear = new Date().getUTCFullYear() + 1; + const proposal = { + title: `[${id}] e2e create flow`, + brief: "e2e brief", + category: "Community", // .anticon-team + targetAmount: 5, + body: `#### e2e Proposal ${id} {enter} **created** ${time} `, + team: [ + { + name: "Alisha Endtoend", + title: "QA Robot0", + ethAddress: `0x0000${randomEthHex}0000`, + emailAddress: `qa.alisha.${id}@grant.io` + }, + { + name: "Billy Endtoend", + title: "QA Robot1", + ethAddress: `0x1111${randomEthHex}1111`, + emailAddress: `qa.billy.${id}@grant.io` + } + ], + milestones: [ + { + title: `e2e Milestone ${id} 0`, + body: `e2e Milestone ${id} {enter} body 0`, + date: { + y: nextYear, + m: "Jan", + expect: "January " + nextYear + } + }, + { + title: `e2e Milestone ${id} 1`, + body: `e2e Milestone ${id} {enter} body 1`, + date: { + y: nextYear, + m: "Feb", + expect: "February " + nextYear + } + } + ] + }; + + afterEach(function() { + if (this.currentTest.state === "failed") { + (Cypress as any).runner.stop(); + } + }); + + context("create flow wizard", () => { + it("authenticates and creates if necessary", () => { + authenticateUser(cy, 0); + }); + + it("create flow step 1", () => { + cy.get('[href="/create"]').click(); + syncTimeWithEvm(cy); + cy.get('.CreateFlow input[name="title"]').type(proposal.title); + cy.get('.CreateFlow textarea[name="brief"]').type(proposal.brief); + cy.contains("Select a category").click(); + cy.get(".ant-select-dropdown li .anticon-team").click(); + cy.get('.CreateFlow input[name="amountToRaise"]').type( + "" + proposal.targetAmount + ); + cy.wait(1000); + cy.contains(".CreateFlow-footer-button", "Continue").click(); + }); + + it("create flow step 2", () => { + cy.get("button.TeamForm-add").click(); + cy.get('.TeamMember-info input[name="name"]').type(proposal.team[1].name); + cy.get('.TeamMember-info input[name="title"]').type( + proposal.team[1].title + ); + cy.get('.TeamMember-info input[name="ethAddress"]').type( + proposal.team[1].ethAddress + ); + cy.get('.TeamMember-info input[name="emailAddress"]').type( + proposal.team[1].emailAddress + ); + cy.get("button") + .contains("Save changes") + .click({ force: true }); + cy.wait(1000); + cy.contains(".CreateFlow-footer-button", "Continue").click(); + }); + + it("create flow step 3", () => { + cy.get(".DraftEditor-editorContainer > div").type(proposal.body); + cy.get(".mde-tabs > :nth-child(2)").click(); + cy.wait(1000); + cy.contains(".CreateFlow-footer-button", "Continue").click(); + }); + + it("create flow step 4", () => { + cy.get('input[name="title"]').type(proposal.milestones[0].title); + cy.get('textarea[name="body"]').type(proposal.milestones[0].body); + cy.get('input[placeholder="Expected completion date"]').click(); + cy.get(".ant-calendar-month-panel-next-year-btn").click(); + cy.get(".ant-calendar-month-panel-month") + .contains(proposal.milestones[0].date.m) + .click(); + cy.get(".ant-calendar-picker-input").should( + "have.value", + proposal.milestones[0].date.expect + ); + cy.get("button") + .contains("Add another milestone") + .click({ force: true }); + cy.get('input[name="title"]') + .eq(1) + .type(proposal.milestones[1].title); + cy.get('textarea[name="body"]') + .eq(1) + .type(proposal.milestones[1].body); + cy.get('input[placeholder="Expected completion date"]') + .eq(1) + .click(); + cy.get(".ant-calendar-month-panel-next-year-btn").click(); + cy.get(".ant-calendar-month-panel-month") + .contains(proposal.milestones[1].date.m) + .click(); + cy.get(".ant-calendar-picker-input") + .eq(1) + .should("have.value", proposal.milestones[1].date.expect); + cy.wait(1000); + cy.contains(".CreateFlow-footer-button", "Continue").click(); + }); + + it("create flow step 5", () => { + cy.window() + .then(w => (w as any).web3.eth.getAccounts()) + .then(accts => { + cy.get('input[name="payOutAddress"]').type(accts[0]); + cy.get("button") + .contains("Add another trustee") + .click({ force: true }); + cy.get( + 'input[placeholder="0x8B0B72F8bDE212991135668922fD5acE557DE6aB"]' + ) + .eq(1) + .type(accts[1]); + cy.get('input[name="deadline"][value="2592000"]').click({ + force: true + }); + cy.get('input[name="milestoneDeadline"][value="259200"]').click({ + force: true + }); + }); + cy.wait(1000); + cy.contains(".CreateFlow-footer-button", "Continue").click(); + }); + + it("publishes the proposal", () => { + cy.get("button") + .contains("Publish") + .click(); + cy.get(".CreateFinal-loader-text").contains("Deploying contract..."); + cy.get(".CreateFinal-message-text a", { timeout: 20000 }) + .contains("Click here") + .click(); + cy.get(".Proposal-top-main-title").contains(proposal.title); + }); + }); +}); diff --git a/e2e/cypress/integration/create.fund.cancel.spec.ts b/e2e/cypress/integration/create.fund.cancel.spec.ts new file mode 100644 index 00000000..85250d42 --- /dev/null +++ b/e2e/cypress/integration/create.fund.cancel.spec.ts @@ -0,0 +1,61 @@ +/// +import { + loadWeb3, + increaseTime, + syncTimeWithEvm, + randomString +} from "../helpers"; +import { createDemoProposal, authenticateUser } from "../parts"; + +describe("create.fund.cancel", () => { + const id = randomString(); + const title = `[${id}] e2e create fund cancel`; + const amount = "1"; + + afterEach(function() { + if (this.currentTest.state === "failed") { + (Cypress as any).runner.stop(); + } + }); + + it("authenticates and creates if necessary", () => { + authenticateUser(cy, 0); + }); + + it("create demo proposal", () => { + createDemoProposal(cy, title, amount); + }); + + it("funds the proposal with account 5", () => { + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) })); + cy.get(".ant-input", { timeout: 20000 }).type(amount); + cy.get(".ant-form > .ant-btn").click(); + cy.get(".ProposalCampaignBlock-fundingOver", { timeout: 20000 }).contains( + "Proposal has been funded" + ); + }); + + it("cancels the proposal (refund contributors)", () => { + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) })); + cy.contains(".Proposal-top-main-menu > .ant-btn", "Actions").click(); + cy.contains(".ant-dropdown-menu-item", "Refund contributors").click(); + cy.contains(".ant-modal-footer > div button", "Confirm").click(); + cy.get(".ant-modal-wrap", { timeout: 20000 }).should("not.be.visible"); + cy.contains(".Proposal-top-main-menu > .ant-btn", "Actions").click(); + cy.contains(".ant-dropdown-menu-item", "Refund contributors").should( + "have.attr", + "aria-disabled", + "true" + ); + }); + + it("refunds the contributor (account 5)", () => { + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) })); + cy.contains(".ant-tabs-nav > :nth-child(1) > :nth-child(4)", "Refund", { + timeout: 20000 + }).click(); + // force disables cypress' auto scrolling which messes up UI in this case + cy.contains(".ant-btn", "Get your refund").click({ force: true }); + cy.contains("body", "Your refund has been processed", { timeout: 20000 }); + }); +}); diff --git a/e2e/cypress/integration/create.fund.complete.minority-no-votes.spec.ts b/e2e/cypress/integration/create.fund.complete.minority-no-votes.spec.ts new file mode 100644 index 00000000..9fa5580a --- /dev/null +++ b/e2e/cypress/integration/create.fund.complete.minority-no-votes.spec.ts @@ -0,0 +1,126 @@ +/// +import { + loadWeb3, + randomString, + syncTimeWithEvm, + increaseTime +} from "../helpers"; +import { createDemoProposal, fundProposal, authenticateUser } from "../parts"; + +describe("create.fund.complete.minority-no-votes", () => { + const id = randomString(); + const title = `[${id}] e2e minority no-votes complete`; + const amount = "1"; + + afterEach(function() { + if (this.currentTest.state === "failed") { + //(Cypress as any).runner.stop(); + this.skip(); + } + }); + + it("authenticates and creates if necessary", () => { + authenticateUser(cy, 0); + }); + + it("creates demo proposal", () => { + createDemoProposal(cy, title, amount); + }); + + it("funds the proposal from accounts 5, 6 & 7", () => { + fundProposal(cy, 5, 0.1); + fundProposal(cy, 6, 0.2); + fundProposal(cy, 7, 0.7); + cy.get(".ProposalCampaignBlock-fundingOver", { timeout: 20000 }).contains( + "Proposal has been funded" + ); + }); + + it("receives initial payout", () => { + // MILESTONE 1 + syncTimeWithEvm(cy); + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) })); + cy.get(".MilestoneAction-top > div > .ant-btn", { timeout: 20000 }).click(); + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Receive initial payout", + { timeout: 20000 } + ).click(); + }); + + it("requests milestone 2 payout", () => { + // MILESTONE 2 + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Request milestone payout", + { timeout: 20000 } + ).click(); + cy.contains(".MilestoneAction-progress-text", "voted against payout", { + timeout: 20000 + }); + }); + + it("minority funder (acct 5) votes no", () => { + // VOTE NO + syncTimeWithEvm(cy); + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) })); + cy.contains(".ant-btn", "Vote against payout", { timeout: 20000 }) + .click() + .should("have.class", "ant-btn-loading"); + cy.contains(".ant-btn", "Revert vote against payout", { timeout: 20000 }); + }); + + it("expires milestone 2 voting period & receives payout", () => { + // EXPIRE + increaseTime(cy, 70000); + // RECEIVE PAYOUT + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) })); + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Receive milestone payout", + { timeout: 20000 } + ).click(); + }); + + it("requests milestone 3 payout", () => { + // MILESTONE 3 + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Request milestone payout", + { timeout: 20000 } + ).click(); + cy.contains(".MilestoneAction-progress-text", "voted against payout", { + timeout: 20000 + }); + }); + + it("minority funder (acct 5) votes no", () => { + // VOTE NO + syncTimeWithEvm(cy); + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) })); + cy.contains(".ant-btn", "Vote against payout", { timeout: 20000 }) + .click() + .should("have.class", "ant-btn-loading"); + cy.contains(".ant-btn", "Revert vote against payout", { timeout: 20000 }); + }); + + it("expires milestone 3 voting period & receives payout", () => { + // EXPIRE + increaseTime(cy, 70000); + // RECEIVE PAYOUT + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) })); + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Receive milestone payout", + { timeout: 20000 } + ).click(); + }); + + it("should not have receive button", () => { + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Receive milestone payout", + { timeout: 20000 } + ).should("not.exist"); + }); +}); diff --git a/e2e/cypress/integration/create.fund.complete.spec.ts b/e2e/cypress/integration/create.fund.complete.spec.ts new file mode 100644 index 00000000..ae819fe0 --- /dev/null +++ b/e2e/cypress/integration/create.fund.complete.spec.ts @@ -0,0 +1,103 @@ +/// +import { + loadWeb3, + increaseTime, + syncTimeWithEvm, + randomString +} from "../helpers"; +import { createDemoProposal, authenticateUser } from "../parts"; + +describe("create.fund.complete", () => { + const id = randomString(); + const title = `[${id}] e2e create fund complete`; + const amount = "1"; + + afterEach(function() { + if (this.currentTest.state === "failed") { + //(Cypress as any).runner.stop(); + this.skip(); + } + }); + + it("authenticates and creates if necessary", () => { + authenticateUser(cy, 0); + }); + + it("create demo proposal", () => { + createDemoProposal(cy, title, amount); + }); + + it("funds the proposal with account 5", () => { + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) })); + cy.get(".ant-input", { timeout: 20000 }).type(amount); + cy.get(".ant-form > .ant-btn").click(); + cy.get(".ProposalCampaignBlock-fundingOver", { timeout: 20000 }).contains( + "Proposal has been funded" + ); + }); + + it("receives initial payout", () => { + // MILESTONE 1 + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) })); + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Request initial payout", + { timeout: 20000 } + ) + .as("RequestPayout") + .click(); + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Receive initial payout", + { timeout: 20000 } + ).click(); + }); + + it("requests and receives milestone 2 payout", () => { + // MILESTONE 2 + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Request milestone payout", + { timeout: 20000 } + ).click(); + cy.contains(".MilestoneAction-progress-text", "voted against payout", { + timeout: 20000 + }); + // EXPIRE + increaseTime(cy, 70000); + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) })); + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Receive milestone payout", + { timeout: 20000 } + ).click(); + }); + + it("requests and receives milestone 3 payout", () => { + // MILESTONE 3 + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Request milestone payout", + { timeout: 20000 } + ).click(); + cy.contains(".MilestoneAction-progress-text", "voted against payout", { + timeout: 20000 + }); + // EXPIRE + increaseTime(cy, 70000); + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) })); + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Receive milestone payout", + { timeout: 20000 } + ).click(); + }); + + it("should not have receive button", () => { + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Receive milestone payout", + { timeout: 20000 } + ).should("not.exist"); + }); +}); diff --git a/e2e/cypress/integration/create.fund.ms2.majority-no-vote.spec.ts b/e2e/cypress/integration/create.fund.ms2.majority-no-vote.spec.ts new file mode 100644 index 00000000..41b40f77 --- /dev/null +++ b/e2e/cypress/integration/create.fund.ms2.majority-no-vote.spec.ts @@ -0,0 +1,60 @@ +/// +import { loadWeb3, randomString, syncTimeWithEvm } from "../helpers"; +import { createDemoProposal, fundProposal, authenticateUser } from "../parts"; + +describe("create.fund.ms2.majority-no-vote", () => { + const id = randomString(); + const title = `[${id}] e2e ms2 majority no-vote`; + const amount = "1"; + + it("authenticates and creates if necessary", () => { + authenticateUser(cy, 0); + }); + + it("creates demo proposal", () => { + createDemoProposal(cy, title, amount); + }); + + it("fund the proposal with 5th account", () => { + fundProposal(cy, 5, 1); + cy.get(".ProposalCampaignBlock-fundingOver", { timeout: 20000 }).contains( + "Proposal has been funded" + ); + }); + + it("receives initial payout for milestone 1", () => { + // MILESTONE 1 + syncTimeWithEvm(cy); + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) })); + cy.get(".MilestoneAction-top > div > .ant-btn", { timeout: 20000 }).click(); + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Receive initial payout", + { timeout: 20000 } + ).click(); + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Request milestone payout", + { timeout: 20000 } + ); + }); + + it("requests milestone 2 payout", () => { + // MILESTONE 2 + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Request milestone payout", + { timeout: 20000 } + ).click(); + cy.contains(".MilestoneAction-progress-text", "voted against payout", { + timeout: 20000 + }); + }); + + it("vote against milestone 2 payout as account 5", () => { + syncTimeWithEvm(cy); + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) })); + cy.contains(".ant-btn", "Vote against payout", { timeout: 20000 }).click(); + cy.contains(".ant-btn", "Revert vote against payout", { timeout: 20000 }); + }); +}); diff --git a/e2e/cypress/integration/create.fund.ms2.no-vote.re-vote.spec.ts b/e2e/cypress/integration/create.fund.ms2.no-vote.re-vote.spec.ts new file mode 100644 index 00000000..c361e1c5 --- /dev/null +++ b/e2e/cypress/integration/create.fund.ms2.no-vote.re-vote.spec.ts @@ -0,0 +1,87 @@ +/// +import { + loadWeb3, + randomString, + syncTimeWithEvm, + increaseTime +} from "../helpers"; +import { createDemoProposal, fundProposal, authenticateUser } from "../parts"; + +describe("create.fund.ms2.no-vote.re-vote", () => { + const id = randomString(); + const title = `[${id}] e2e ms2 no-vote expire re-vote`; + const amount = "1"; + + afterEach(function() { + if (this.currentTest.state === "failed") { + //(Cypress as any).runner.stop(); + this.skip(); + } + }); + + it("authenticates and creates if necessary", () => { + authenticateUser(cy, 0); + }); + + it("creates demo proposal", () => { + createDemoProposal(cy, title, amount); + }); + + it("fund the proposal with 5th account", () => { + fundProposal(cy, 5, 1); + cy.get(".ProposalCampaignBlock-fundingOver", { timeout: 20000 }).contains( + "Proposal has been funded" + ); + }); + + it("receives initial payout for milestone 1", () => { + // MILESTONE 1 + syncTimeWithEvm(cy); + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) })); + cy.get(".MilestoneAction-top > div > .ant-btn", { timeout: 20000 }).click(); + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Receive initial payout", + { timeout: 20000 } + ).click(); + }); + + it("request milestone 2 payout", () => { + // MILESTONE 2 + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Request milestone payout", + { timeout: 20000 } + ).click(); + cy.contains(".MilestoneAction-progress-text", "voted against payout", { + timeout: 20000 + }); + }); + + it("vote against milestone 2 payout as 5th account", () => { + // reload page with 5th account + syncTimeWithEvm(cy); + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) })); + cy.contains(".ant-btn", "Vote against payout", { timeout: 20000 }) + .click() + .should("have.class", "ant-btn-loading"); + cy.contains(".ant-btn", "Revert vote against payout", { timeout: 20000 }); + }); + + it("milestone 2 vote expires and payout is requested again", () => { + // EXPIRE + increaseTime(cy, 70000); + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) })); + cy.contains("Payout was voted against"); + // RE-REQUEST PAYOUT + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Request milestone payout", + { timeout: 20000 } + ).click(); + // TODO: fix this bug (the following fails) + cy.contains(".MilestoneAction-progress-text", "voted against payout", { + timeout: 20000 + }); + }); +}); diff --git a/e2e/cypress/integration/create.fund.ms2.refund-after-payout.spec.ts b/e2e/cypress/integration/create.fund.ms2.refund-after-payout.spec.ts new file mode 100644 index 00000000..f52f2fb0 --- /dev/null +++ b/e2e/cypress/integration/create.fund.ms2.refund-after-payout.spec.ts @@ -0,0 +1,69 @@ +/// +import { + loadWeb3, + randomString, + syncTimeWithEvm, + increaseTime +} from "../helpers"; +import { createDemoProposal, fundProposal, authenticateUser } from "../parts"; + +describe("create.fund.ms2.refund-after-payout", () => { + const id = randomString(); + const title = `[${id}] e2e ms2 refund after payout`; + const amount = "1"; + + afterEach(function() { + if (this.currentTest.state === "failed") { + //(Cypress as any).runner.stop(); + this.skip(); + } + }); + + it("authenticates and creates if necessary", () => { + authenticateUser(cy, 0); + }); + + it("creates demo proposal", () => { + createDemoProposal(cy, title, amount); + }); + + it("fund the proposal with account 5", () => { + fundProposal(cy, 5, 1); + cy.get(".ProposalCampaignBlock-fundingOver", { timeout: 20000 }).contains( + "Proposal has been funded" + ); + }); + + it("receives initial payout for milestone 1", () => { + // MILESTONE 1 + syncTimeWithEvm(cy); + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) })); + cy.get(".MilestoneAction-top > div > .ant-btn", { timeout: 20000 }).click(); + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Receive initial payout", + { timeout: 20000 } + ).click(); + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Request milestone payout", + { timeout: 20000 } + ); + }); + + it("majority refund vote and get refund (account 5)", () => { + // REFUND + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) })); + cy.contains(".ant-tabs-nav > :nth-child(1) > :nth-child(4)", "Refund", { + timeout: 20000 + }).click(); + // INCREASE TIME + increaseTime(cy, 70000); + // force disables cypress' auto scrolling which messes up UI in this case + cy.contains(".ant-btn", "Vote for refund").click({ force: true }); + cy.contains(".ant-btn", "Get your refund", { timeout: 20000 }).click({ + force: true + }); + cy.contains("body", "Your refund has been processed", { timeout: 20000 }); + }); +}); diff --git a/e2e/cypress/integration/create.fund.ms2.revert-no-vote.spec.ts b/e2e/cypress/integration/create.fund.ms2.revert-no-vote.spec.ts new file mode 100644 index 00000000..ca7ddf7a --- /dev/null +++ b/e2e/cypress/integration/create.fund.ms2.revert-no-vote.spec.ts @@ -0,0 +1,93 @@ +/// +import { + loadWeb3, + randomString, + syncTimeWithEvm, + increaseTime +} from "../helpers"; +import { createDemoProposal, fundProposal, authenticateUser } from "../parts"; + +describe("create.fund.ms2.revert-no-vote", () => { + const id = randomString(); + const title = `[${id}] e2e ms2 revert no-vote`; + const amount = "1"; + + afterEach(function() { + if (this.currentTest.state === "failed") { + //(Cypress as any).runner.stop(); + this.skip(); + } + }); + + it("authenticates and creates if necessary", () => { + authenticateUser(cy, 0); + }); + + it("creates demo proposal", () => { + createDemoProposal(cy, title, amount); + }); + + it("funds the proposal with 5th account", () => { + fundProposal(cy, 5, 1); + cy.get(".ProposalCampaignBlock-fundingOver", { timeout: 20000 }).contains( + "Proposal has been funded" + ); + }); + + it("receives initial payout for milestone 1", () => { + // MILESTONE 1 + syncTimeWithEvm(cy); + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) })); + cy.get(".MilestoneAction-top > div > .ant-btn", { timeout: 20000 }).click(); + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Receive initial payout", + { timeout: 20000 } + ).click(); + }); + + it("requests milestone 2 payout", () => { + // MILESTONE 2 + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Request milestone payout", + { timeout: 20000 } + ).click(); + cy.contains(".MilestoneAction-progress-text", "voted against payout", { + timeout: 20000 + }); + }); + + it("votes against milestone 2 payout as account 5 and then reverts the vote", () => { + // NO VOTE... REVERT + syncTimeWithEvm(cy); + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(5) })); + cy.contains(".ant-btn", "Vote against payout", { timeout: 20000 }) + .click() + .should("have.class", "ant-btn-loading"); + cy.contains(".ant-btn", "Revert vote against payout", { timeout: 20000 }) + .click() + .should("have.class", "ant-btn-loading"); + cy.contains(".ant-btn", "Vote against payout", { timeout: 20000 }); + }); + + it("milestone 2 vote expires and payout is received", () => { + // EXPIRE + increaseTime(cy, 70000); + // PAYOUT + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(0) })); + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Receive milestone payout" + ).click(); + }); + + it("milestone 3 becomes active", () => { + // MILESTONE 3 + cy.contains( + ".MilestoneAction-top > div > .ant-btn", + "Request milestone payout", + { timeout: 20000 } + ); + }); +}); diff --git a/e2e/cypress/integration/create.spec.ts b/e2e/cypress/integration/create.spec.ts deleted file mode 100644 index 98ef3477..00000000 --- a/e2e/cypress/integration/create.spec.ts +++ /dev/null @@ -1,192 +0,0 @@ -/// -import { loadWeb3, randomString, randomHex } from "../helpers"; - -describe("create proposal", () => { - it("should load and be able to browse pages", () => { - cy.visit("http://localhost:3000/create", { onBeforeLoad: loadWeb3 }); - - // populate ethAccounts - cy.wait(1000); - cy.window() - .then(w => (w as any).web3.eth.getAccounts()) - .as("EthAccounts"); - - // demo proposal - // cy.get("button.CreateFlow-footer-example").click(); - - const time = new Date().toLocaleString(); - const id = randomString(); - const randomEthHex = randomHex(32); - const nextYear = new Date().getUTCFullYear() + 1; - const proposal = { - title: "e2e - smoke - create " + time, - brief: "e2e brief", - category: "Community", // .anticon-team - targetAmount: 5, - body: `#### e2e Proposal ${id} {enter} **created** ${time} `, - team: [ - { - name: "Alisha Endtoend", - title: "QA Robot0", - ethAddress: `0x0000${randomEthHex}0000`, - emailAddress: `qa.alisha.${id}@grant.io` - }, - { - name: "Billy Endtoend", - title: "QA Robot1", - ethAddress: `0x1111${randomEthHex}1111`, - emailAddress: `qa.billy.${id}@grant.io` - } - ], - milestones: [ - { - title: `e2e Milestone ${id} 0`, - body: `e2e Milestone ${id} {enter} body 0`, - date: { - y: nextYear, - m: "Jan", - expect: "January " + nextYear - } - }, - { - title: `e2e Milestone ${id} 1`, - body: `e2e Milestone ${id} {enter} body 1`, - date: { - y: nextYear, - m: "Feb", - expect: "February " + nextYear - } - } - ] - }; - - // step 1 - cy.get('.CreateFlow input[name="title"]', { timeout: 20000 }).type( - proposal.title - ); - cy.get('.CreateFlow textarea[name="brief"]').type(proposal.brief); - cy.contains("Select a category").click(); - cy.get(".ant-select-dropdown li .anticon-team").click(); - cy.get('.CreateFlow input[name="amountToRaise"]').type( - "" + proposal.targetAmount - ); - cy.get(".CreateFlow-footer-button") - .contains("Continue") - .as("Continue") - .click(); - - // step 2 - cy.get('.TeamMember-info input[name="name"]').type(proposal.team[0].name); - cy.get('.TeamMember-info input[name="title"]').type(proposal.team[0].title); - cy.get("@EthAccounts").then(accts => { - cy.get('.TeamMember-info input[name="ethAddress"]').type( - accts[0].toString() - ); - }); - cy.get('.TeamMember-info input[name="emailAddress"]').type( - proposal.team[0].emailAddress - ); - cy.get("button") - .contains("Save changes") - .click({ force: true }); - - cy.get("button.TeamForm-add").click(); - cy.get('.TeamMember-info input[name="name"]').type(proposal.team[1].name); - cy.get('.TeamMember-info input[name="title"]').type(proposal.team[1].title); - cy.get("@EthAccounts").then(accts => { - cy.get('.TeamMember-info input[name="ethAddress"]').type( - accts[1].toString() - ); - }); - cy.get('.TeamMember-info input[name="emailAddress"]').type( - proposal.team[1].emailAddress - ); - cy.get("button") - .contains("Save changes") - .click({ force: true }); - - cy.wait(1000); - cy.get("@Continue").click(); - - // step 3 - cy.get(".DraftEditor-editorContainer > div").type(proposal.body); - cy.get(".mde-tabs > :nth-child(2)").click(); - - cy.wait(1000); - cy.get("@Continue").click(); - - // step 4 - cy.get('input[name="title"]').type(proposal.milestones[0].title); - cy.get('textarea[name="body"]').type(proposal.milestones[0].body); - cy.get('input[placeholder="Expected completion date"]').click(); - cy.get(".ant-calendar-month-panel-next-year-btn").click(); - cy.get(".ant-calendar-month-panel-month") - .contains(proposal.milestones[0].date.m) - .click(); - cy.get(".ant-calendar-picker-input").should( - "have.value", - proposal.milestones[0].date.expect - ); - - cy.get("button") - .contains("Add another milestone") - .click({ force: true }); - - cy.get('input[name="title"]') - .eq(1) - .type(proposal.milestones[1].title); - cy.get('textarea[name="body"]') - .eq(1) - .type(proposal.milestones[1].body); - cy.get('input[placeholder="Expected completion date"]') - .eq(1) - .click(); - cy.get(".ant-calendar-month-panel-next-year-btn").click(); - cy.get(".ant-calendar-month-panel-month") - .contains(proposal.milestones[1].date.m) - .click(); - cy.get(".ant-calendar-picker-input") - .eq(1) - .should("have.value", proposal.milestones[1].date.expect); - - cy.wait(1000); - cy.get("@Continue").click(); - - // step 5 - cy.window() - .then(w => (w as any).web3.eth.getAccounts()) - .then(accts => { - cy.get('input[name="payOutAddress"]').type(accts[0]); - cy.get("button") - .contains("Add another trustee") - .click({ force: true }); - cy.get( - 'input[placeholder="0x8B0B72F8bDE212991135668922fD5acE557DE6aB"]' - ) - .eq(1) - .type(accts[1]); - cy.get('input[name="deadline"][value="2592000"]').click({ - force: true - }); - cy.get('input[name="milestoneDeadline"][value="259200"]').click({ - force: true - }); - }); - - cy.wait(1000); - cy.get("@Continue").click(); - - // final - cy.get("button") - .contains("Publish") - .click(); - cy.get(".CreateFinal-loader-text").contains("Deploying contract..."); - - cy.get(".CreateFinal-message-text a", { timeout: 20000 }) - .contains("Click here") - .click(); - - // done - cy.get(".Proposal-top-main-title").contains(proposal.title); - }); -}); diff --git a/e2e/cypress/integration/sandbox.ts b/e2e/cypress/integration/sandbox.ts new file mode 100644 index 00000000..48267885 --- /dev/null +++ b/e2e/cypress/integration/sandbox.ts @@ -0,0 +1,15 @@ +/// +import { + loadWeb3, + increaseTime, + randomString, + syncTimeWithEvm +} from "../helpers"; + +// describe("sandbox", () => { +// it("how to increase time", () => { +// cy.visit("http://localhost:3000", { onBeforeLoad: loadWeb3(0) }); +// // increase time on browser and ganache +// increaseTime(cy, 60000); +// }); +// }); diff --git a/e2e/cypress/parts.ts b/e2e/cypress/parts.ts new file mode 100644 index 00000000..4ef50564 --- /dev/null +++ b/e2e/cypress/parts.ts @@ -0,0 +1,107 @@ +/// +import { syncTimeWithEvm, loadWeb3, testAccounts } from "./helpers"; + +export const authenticateUser = ( + cy: Cypress.Chainable, + accountIndex: number +) => { + const name = `Qual Itty ${accountIndex}`; + const ethAccount = testAccounts[accountIndex][0]; + const title = `QA Robot ${accountIndex}`; + const email = `qa.robot.${accountIndex}@grant.io`; + cy.visit("http://localhost:3000", { onBeforeLoad: loadWeb3(accountIndex) }); + syncTimeWithEvm(cy); + cy.get(".AuthButton").click(); + cy.request({ + url: `http://localhost:5000/api/v1/users/${ethAccount}`, + method: "GET", + failOnStatusCode: false + }) + .its("status") + .then(status => { + if (status === 200) { + cy.contains("button", "Prove identity").click(); + } else { + cy.get("input[name='name']").type(name); + cy.get("input[name='title']").type(title); + cy.get("input[name='email']").type(email); + cy.contains("button", "Claim Identity").click(); + } + cy.contains(".ProfileUser", email); + }); +}; + +export const createDemoProposal = ( + cy: Cypress.Chainable, + title: string, + amount: string +) => { + cy.get('[href="/create"]').click(); + + // expects to be @ /create + cy.url().should("contain", "/create"); + + cy.log("CREATE DEMO PROPOSAL", title, amount); + + // demo proposal + cy.get("button.CreateFlow-footer-example").click(); + + // change name + cy.get(".ant-steps > :nth-child(1)").click(); + cy.get('.CreateFlow input[name="title"]', { timeout: 20000 }) + .clear() + .type(title) + .blur(); + cy.get('.CreateFlow input[name="amountToRaise"]') + .clear() + .type(amount) + .blur(); + cy.wait(1000); + + // remove extra trustees + cy.get(".ant-steps > :nth-child(5)").click(); + cy.get( + ":nth-child(11) > .ant-form-item-control-wrapper div > button" + ).click(); + cy.get( + ":nth-child(10) > .ant-form-item-control-wrapper div > button" + ).click(); + cy.get(":nth-child(9) > .ant-form-item-control-wrapper div > button").click(); + cy.get(":nth-child(8) > .ant-form-item-control-wrapper div > button").click(); + cy.get(":nth-child(7) > .ant-form-item-control-wrapper div > button").click(); + cy.wait(1000); + cy.get(".CreateFlow-footer-button") + .contains("Continue") + .click(); + + // final + cy.get("button") + .contains("Publish") + .click(); + cy.get(".CreateFinal-loader-text").contains("Deploying contract..."); + cy.get(".CreateFinal-message-text a", { timeout: 30000 }) + .contains("Click here") + .click(); + + // created + cy.get(".Proposal-top-main-title").contains(title); +}; + +export const fundProposal = ( + cy: Cypress.Chainable, + accountIndex: number, + amount: number +) => { + // expects to be @ /proposals/ + cy.url().should("contain", "/proposals/"); + + // reload page with accountIndex account + syncTimeWithEvm(cy); + cy.url().then(url => cy.visit(url, { onBeforeLoad: loadWeb3(accountIndex) })); + + // fund proposal + cy.get(".ant-input", { timeout: 20000 }).type(amount + ""); + cy.contains(".ant-form > .ant-btn", "Fund this project", { timeout: 20000 }) + .click() + .should("not.have.attr", "loading"); +}; diff --git a/e2e/cypress/typings.d.ts b/e2e/cypress/typings.d.ts index f3f59664..721f4b18 100644 --- a/e2e/cypress/typings.d.ts +++ b/e2e/cypress/typings.d.ts @@ -2,3 +2,7 @@ declare module "*.json" { const value: any; export default value; } + +declare module "eth-sig-util" { + export function signTypedData(k: any, d: any): string; +} diff --git a/e2e/package.json b/e2e/package.json index 641f382c..dd36cf1e 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -11,6 +11,7 @@ "@cypress/webpack-preprocessor": "^2.0.1", "@types/web3": "^1.0.3", "cypress": "^3.1.0", + "eth-sig-util": "^2.1.0", "ts-loader": "^5.0.0", "typescript": "^3.0.3", "web3": "^1.0.0-beta.36", diff --git a/e2e/yarn.lock b/e2e/yarn.lock index 873d4e72..5ee8e43b 100644 --- a/e2e/yarn.lock +++ b/e2e/yarn.lock @@ -1011,6 +1011,16 @@ binary-extensions@^1.0.0: version "1.11.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" +bindings@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7" + +bip66@^1.1.3: + version "1.1.5" + resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22" + dependencies: + safe-buffer "^5.0.1" + bl@^1.0.0: version "1.2.2" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" @@ -1040,7 +1050,7 @@ bn.js@4.11.6: version "4.11.6" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.6, bn.js@^4.4.0: +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.3, bn.js@^4.11.6, bn.js@^4.4.0, bn.js@^4.8.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" @@ -1100,7 +1110,7 @@ brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" -browserify-aes@^1.0.0, browserify-aes@^1.0.4: +browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.0.6: version "1.2.0" resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" dependencies: @@ -1213,7 +1223,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.0.5: +buffer@^5.0.5, buffer@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6" dependencies: @@ -1779,6 +1789,14 @@ domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" +drbg.js@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/drbg.js/-/drbg.js-1.0.1.tgz#3e36b6c42b37043823cdbc332d58f31e2445480b" + dependencies: + browserify-aes "^1.0.6" + create-hash "^1.1.2" + create-hmac "^1.1.4" + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -1820,7 +1838,7 @@ elliptic@6.3.3: hash.js "^1.0.0" inherits "^2.0.1" -elliptic@^6.0.0, elliptic@^6.4.0: +elliptic@^6.0.0, elliptic@^6.2.3, elliptic@^6.4.0: version "6.4.1" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a" dependencies: @@ -1920,6 +1938,46 @@ eth-lib@0.2.7: elliptic "^6.4.0" xhr-request-promise "^0.1.2" +eth-sig-util@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-2.1.0.tgz#33e60e5486897a2ddeb4bf5a0993b2c6d5cc9e19" + dependencies: + buffer "^5.2.1" + elliptic "^6.4.0" + ethereumjs-abi "0.6.5" + ethereumjs-util "^5.1.1" + tweetnacl "^1.0.0" + tweetnacl-util "^0.15.0" + +ethereumjs-abi@0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.5.tgz#5a637ef16ab43473fa72a29ad90871405b3f5241" + dependencies: + bn.js "^4.10.0" + ethereumjs-util "^4.3.0" + +ethereumjs-util@^4.3.0: + version "4.5.0" + resolved "http://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz#3e9428b317eebda3d7260d854fddda954b1f1bc6" + dependencies: + bn.js "^4.8.0" + create-hash "^1.1.2" + keccakjs "^0.2.0" + rlp "^2.0.0" + secp256k1 "^3.0.1" + +ethereumjs-util@^5.1.1: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz#3e0c0d1741471acf1036052d048623dee54ad642" + dependencies: + bn.js "^4.11.0" + create-hash "^1.1.2" + ethjs-util "^0.1.3" + keccak "^1.0.2" + rlp "^2.0.0" + safe-buffer "^5.1.1" + secp256k1 "^3.0.1" + ethers@4.0.0-beta.1: version "4.0.0-beta.1" resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.0-beta.1.tgz#0648268b83e0e91a961b1af971c662cdf8cbab6d" @@ -1942,6 +2000,13 @@ ethjs-unit@0.1.6: bn.js "4.11.6" number-to-bn "1.7.0" +ethjs-util@^0.1.3: + version "0.1.6" + resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536" + dependencies: + is-hex-prefixed "1.0.0" + strip-hex-prefix "1.0.0" + eventemitter3@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.1.1.tgz#47786bdaa087caf7b1b75e73abc5c7d540158cd0" @@ -2872,7 +2937,16 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -keccakjs@^0.2.1: +keccak@^1.0.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/keccak/-/keccak-1.4.0.tgz#572f8a6dbee8e7b3aa421550f9e6408ca2186f80" + dependencies: + bindings "^1.2.1" + inherits "^2.0.3" + nan "^2.2.1" + safe-buffer "^5.1.0" + +keccakjs@^0.2.0, keccakjs@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/keccakjs/-/keccakjs-0.2.1.tgz#1d633af907ef305bbf9f2fa616d56c44561dfa4d" dependencies: @@ -3238,6 +3312,10 @@ nan@^2.0.8, nan@^2.3.3, nan@^2.9.2: version "2.11.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.0.tgz#574e360e4d954ab16966ec102c0c049fd961a099" +nan@^2.2.1: + version "2.11.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766" + nano-json-stream-parser@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz#0cc8f6d0e2b622b479c40d499c46d64b755c6f5f" @@ -3926,6 +4004,12 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +rlp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.1.0.tgz#e4f9886d5a982174f314543831e36e1a658460f9" + dependencies: + safe-buffer "^5.1.1" + run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" @@ -3990,6 +4074,19 @@ scryptsy@^1.2.1: dependencies: pbkdf2 "^3.0.3" +secp256k1@^3.0.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.5.2.tgz#f95f952057310722184fe9c914e6b71281f2f2ae" + dependencies: + bindings "^1.2.1" + bip66 "^1.1.3" + bn.js "^4.11.3" + create-hash "^1.1.2" + drbg.js "^1.0.1" + elliptic "^6.2.3" + nan "^2.2.1" + safe-buffer "^5.1.0" + seek-bzip@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.5.tgz#cfe917cb3d274bcffac792758af53173eb1fabdc" @@ -4526,10 +4623,18 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tweetnacl-util@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.0.tgz#4576c1cee5e2d63d207fee52f1ba02819480bc75" + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" +tweetnacl@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.0.tgz#713d8b818da42068740bf68386d0479e66fc8a7b" + type-is@~1.6.15, type-is@~1.6.16: version "1.6.16" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" diff --git a/frontend/client/lib/getContract.ts b/frontend/client/lib/getContract.ts index 3d9bf4cf..67dcf31a 100644 --- a/frontend/client/lib/getContract.ts +++ b/frontend/client/lib/getContract.ts @@ -16,7 +16,13 @@ const getContractInstance = async ( deployedAddress = deployedAddress || contractDefinition.networks[networkId].address; // create the instance - return new web3.eth.Contract(contractDefinition.abi, deployedAddress); + const contract = new web3.eth.Contract(contractDefinition.abi, deployedAddress); + + // use gas from e2e injected window.web3.provider + if ((web3.currentProvider as any)._e2eContractGas) { + contract.options.gas = (web3.currentProvider as any)._e2eContractGas; + } + return contract; }; export default getContractInstance; diff --git a/frontend/client/modules/web3/actions.ts b/frontend/client/modules/web3/actions.ts index e411ee13..cf6c35e1 100644 --- a/frontend/client/modules/web3/actions.ts +++ b/frontend/client/modules/web3/actions.ts @@ -508,7 +508,7 @@ export function signData(data: object, dataTypes: object, primaryType: string) { primaryType, }; - (web3.currentProvider as any).sendAsync( + (web3.currentProvider as any).send( { method: 'eth_signTypedData_v3', params: [accounts[0], JSON.stringify(rawTypedData)], diff --git a/frontend/package.json b/frontend/package.json index 9ef3cc1f..d4fb59f1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,7 @@ "heroku-postbuild": "yarn build", "tsc": "tsc", "link-contracts": "cd client/lib && ln -s ../../build/contracts contracts", - "ganache": "ganache-cli -b 5", + "ganache": "ganache-cli -b 5 -s testGrantIo -e 1000", "truffle": "truffle exec ./bin/init-truffle.js && cd client/lib/contracts && truffle console", "storybook": "start-storybook -p 9001 -c .storybook" },