Skip to content

Commit

Permalink
Cypress can now sign programmatically (#367)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tbaut authored Sep 29, 2023
1 parent d932bb9 commit 7bcb997
Show file tree
Hide file tree
Showing 17 changed files with 237 additions and 50 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,5 @@ builds
!.yarn/versions

# cypress
cypress/screenshots
cypress/videos
packages/ui/cypress/screenshots/*
packages/ui/cypress/videos/*
19 changes: 12 additions & 7 deletions packages/ui/cypress/fixtures/injectedAccounts.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { InjectedAccount } from '@polkadot/extension-inject/types'

export interface InjectedAccountWitMnemonic extends InjectedAccount {
mnemonic: string
}
export const injectedAccounts = [
{
address: '7NPoMQbiA6trJKkjB35uk96MeJD4PGWkLQLH7k7hXEkZpiba',
name: 'Alice',
type: 'sr25519'
address: '5H679cx9tkuHqyReUgBxeTqXKjVikVwLySDH1buiYuoqhi2w',
name: 'TestAccount 1',
type: 'sr25519',
mnemonic: 'climb worth pioneer mushroom cloth expose tube high half final curtain toward'
},
{
address: '5DqVySMC366P8NRjdyp948TJj64hAvg17eaiEn4ZbuKCNZHc',
name: 'Bob',
type: 'sr25519'
address: '5GCXBrumiRDQ8KQsgbG39HdBNLQKt6XCQbeHZJccGdZbYTgt',
name: 'TestAccount 2',
type: 'sr25519',
mnemonic: 'divorce lottery slender again adapt process slow pigeon suit chase news begin'
}
] as InjectedAccount[]
] as InjectedAccountWitMnemonic[]
84 changes: 70 additions & 14 deletions packages/ui/cypress/support/Extension.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,50 @@
import { Injected, InjectedAccount, InjectedAccounts } from '@polkadot/extension-inject/types'

export interface AuthRequests {
[index: number]: {
id: number
origin: string
resolve: (accountAddresses: string[]) => void
reject: (reason: string) => void
}
import { Injected, InjectedAccounts } from '@polkadot/extension-inject/types'
import { Keyring } from '@polkadot/keyring'
import { InjectedAccountWitMnemonic } from '../fixtures/injectedAccounts'
import { TypeRegistry } from '@polkadot/types'
import { SignerPayloadJSON } from '@polkadot/types/types'
import { cryptoWaitReady } from '@polkadot/util-crypto'

export interface AuthRequest {
id: number
origin: string
resolve: (accountAddresses: string[]) => void
reject: (reason: string) => void
}

export interface TxRequest {
id: number
payload: SignerPayloadJSON
resolve: () => void
reject: (reason: string) => void
}

export type TxRequests = Record<number, TxRequest>
export type AuthRequests = Record<number, AuthRequest>

export type EnableRequest = number

export class Extension {
authRequests: AuthRequests = {}
accounts: InjectedAccount[] = []
accounts: InjectedAccountWitMnemonic[] = []
txRequests: TxRequests = {}
keyring: Keyring | undefined

reset = () => {
this.authRequests = {}
this.accounts = []
this.txRequests = {}
this.keyring = undefined
}

init = (accounts: InjectedAccount[]) => {
init = async (accounts: InjectedAccountWitMnemonic[]) => {
this.reset()
this.accounts = accounts
await cryptoWaitReady()
this.keyring = new Keyring({ type: 'sr25519' })
accounts.forEach(({ mnemonic }) => {
this.keyring?.addFromMnemonic(mnemonic)
})
}

getInjectedEnable = () => {
Expand All @@ -39,11 +61,33 @@ export class Extension {
resolve({
accounts: {
get: () => selectedAccounts,
subscribe: (cb: (accounts: InjectedAccount[]) => void) => cb(selectedAccounts)
subscribe: (cb: (accounts: InjectedAccountWitMnemonic[]) => void) =>
cb(selectedAccounts)
} as unknown as InjectedAccounts,
signer: {
signPayload: (payload: any) => {
return new Promise(() => {})
signPayload: (payload: SignerPayloadJSON) => {
return new Promise((resolve, reject) => {
const id = Date.now()
const res = () => {
const registry = new TypeRegistry()
registry.setSignedExtensions(payload.signedExtensions)
const pair = this.keyring?.getPair(this.accounts[0].address)
if (!pair) {
console.error('Pair not found')
return
}
const signature = registry
.createType('ExtrinsicPayload', payload, {
version: payload.version
})
.sign(pair)
resolve({ id, signature: signature.signature })
}

const rej = (reason: string) => reject(new Error(reason))

this.txRequests[id] = { id, payload, resolve: res, reject: rej }
})
}
}
})
Expand All @@ -70,4 +114,16 @@ export class Extension {
rejectAuth = (id: number, reason: string) => {
this.authRequests[id].reject(reason)
}

getTxRequests = () => {
return this.txRequests
}

approveTx = (id: number) => {
this.txRequests[id].resolve()
}

rejectTx = (id: number, reason: string) => {
this.txRequests[id].reject(reason)
}
}
42 changes: 36 additions & 6 deletions packages/ui/cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// <reference types="cypress" />

import { AuthRequests, Extension } from './Extension'
import { InjectedAccount } from '@polkadot/extension-inject/types'
import { AuthRequests, Extension, TxRequests } from './Extension'
import { InjectedAccountWitMnemonic } from '../fixtures/injectedAccounts'

// ***********************************************
// This example commands.ts shows you how to
Expand Down Expand Up @@ -42,7 +42,7 @@ import { InjectedAccount } from '@polkadot/extension-inject/types'

const extension = new Extension()

Cypress.Commands.add('initExtension', (accounts: InjectedAccount[]) => {
Cypress.Commands.add('initExtension', (accounts: InjectedAccountWitMnemonic[]) => {
cy.log('Initializing extension')
cy.wrap(extension.init(accounts))

Expand All @@ -65,6 +65,18 @@ Cypress.Commands.add('rejectAuth', (id: number, reason: string) => {
return extension.rejectAuth(id, reason)
})

Cypress.Commands.add('getTxRequests', () => {
return cy.wait(500).then(() => cy.wrap(extension.getTxRequests()))
})

Cypress.Commands.add('approveTx', (id: number) => {
return extension.approveTx(id)
})

Cypress.Commands.add('rejectTx', (id: number, reason: string) => {
return extension.rejectTx(id, reason)
})

declare global {
namespace Cypress {
interface Chainable {
Expand All @@ -73,26 +85,44 @@ declare global {
* @param {InjectedAccount[]} accounts - Accounts to load into the extension.
* @example cy.initExtension([{ address: '7NPoMQbiA6trJKkjB35uk96MeJD4PGWkLQLH7k7hXEkZpiba', name: 'Alice', type: 'sr25519'}])
*/
initExtension: (accounts: InjectedAccount[]) => Chainable<AUTWindow>
initExtension: (accounts: InjectedAccountWitMnemonic[]) => Chainable<AUTWindow>
/**
* Read the authentication request queue.
* @example cy.getAuthRequests().then((authQueue) => { cy.wrap(Object.values(authQueue).length).should("eq", 1) })
*/
getAuthRequests: () => Chainable<AuthRequests>
/**
* Authorize a specific request
* @param {number} id - the id of the request to authorize. This is part of the getAuthRequests object response.
* @param {number} id - the id of the request to authorize. This id is part of the getAuthRequests object response.
* @param {string[]} accountAddresses - the account addresses to share with the applications. These addresses must be part of the ones shared in the `initExtension`
* @example cy.enableAuth(1694443839903, ["7NPoMQbiA6trJKkjB35uk96MeJD4PGWkLQLH7k7hXEkZpiba"])
*/
enableAuth: (id: number, accountAddresses: string[]) => void
/**
* Reject a specific request
* @param {number} id - the id of the request to authorize. This is part of the getAuthRequests object response.
* @param {number} id - the id of the request to reject. This id is part of the getAuthRequests object response.
* @param {reason} reason - the reason for the rejection
* @example cy.rejectAuth(1694443839903, "Cancelled")
*/
rejectAuth: (id: number, reason: string) => void
/**
* Read the tx request queue.
* @example cy.getTxRequests().then((txQueue) => { cy.wrap(Object.values(txQueue).length).should("eq", 1) })
*/
getTxRequests: () => Chainable<TxRequests>
/**
* Authorize a specific request
* @param {number} id - the id of the request to approve. This id is part of the getTxRequests object response.
* @example cy.approveTx(1694443839903)
*/
approveTx: (id: number) => void
/**
* Reject a specific request
* @param {number} id - the id of the tx request to reject. This id is part of the getTxRequests object response.
* @param {reason} reason - the reason for the rejection
* @example cy.rejectAuth(1694443839903, "Cancelled")
*/
rejectTx: (id: number, reason: string) => void
}
}
}
3 changes: 3 additions & 0 deletions packages/ui/cypress/support/page-objects/multisigPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const multisigPage = {
newTransactionButton: () => cy.get('[data-cy="button-new-transaction"]')
}
6 changes: 6 additions & 0 deletions packages/ui/cypress/support/page-objects/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const notifications = {
successNotificationIcon: () => cy.get('[data-cy="notification-icon-success"]'),
errorNotificationIcon: () => cy.get('[data-cy="notification-icon-error"]'),
loadingNotificationIcon: () => cy.get('[data-cy="notification-icon-loading"]'),
notificationWrapper: () => cy.get('[data-cy="notification-wrapper"]')
}
6 changes: 6 additions & 0 deletions packages/ui/cypress/support/page-objects/sendTxModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const sendTxModal = {
sendTxTitle: () => cy.get('[data-cy="title-send-tx"]'),
fieldTo: () => cy.get('[data-cy="field-to"]'),
fieldAmount: () => cy.get('[data-cy="field-amount"]'),
buttonSend: () => cy.get('[data-cy="button-send"]')
}
3 changes: 2 additions & 1 deletion packages/ui/cypress/tests/login.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ describe('Connect Account', () => {
})

it('Connects with Alice', () => {
const AliceAddress = Object.values(injectedAccounts)[0].address
cy.getAuthRequests().then((authRequests) => {
const requests = Object.values(authRequests)
// we should have 1 connection request to the extension
cy.wrap(requests.length).should('eq', 1)
// this request should be from the application Multix
cy.wrap(requests[0].origin).should('eq', 'Multix')
// let's allow it for Alice
cy.enableAuth(requests[0].id, [Object.values(injectedAccounts)[0].address])
cy.enableAuth(requests[0].id, [AliceAddress])
// the ui should then move on to connecting to the rpcs
topMenuItems.multiproxySelector().should('be.visible')
})
Expand Down
69 changes: 69 additions & 0 deletions packages/ui/cypress/tests/transactions.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { injectedAccounts } from '../fixtures/injectedAccounts'
import { landingPageUrl } from '../fixtures/landingData'
import { landingPage } from '../support/page-objects/landingPage'
import { multisigPage } from '../support/page-objects/multisigPage'
import { notifications } from '../support/page-objects/notifications'
import { sendTxModal } from '../support/page-objects/sendTxModal'
import { topMenuItems } from '../support/page-objects/topMenuItems'

const AliceAddress = Object.values(injectedAccounts)[0].address

const fillAndSubmitTransactionForm = () => {
sendTxModal.fieldTo().click().type(`${AliceAddress}{enter}`)
sendTxModal.fieldAmount().click().type('0.001')
sendTxModal.buttonSend().should('be.enabled').click()
}

describe('Perform transactions', () => {
beforeEach(() => {
cy.visit(landingPageUrl)
cy.initExtension(injectedAccounts)
topMenuItems.connectButton().click()
landingPage.accountsOrRpcLoader().should('contain', 'Loading accounts')
cy.getAuthRequests().then((authRequests) => {
const requests = Object.values(authRequests)
// we should have 1 connection request to the extension
cy.wrap(requests.length).should('eq', 1)
// this request should be from the application Multix
cy.wrap(requests[0].origin).should('eq', 'Multix')
// let's allow it for Alice
cy.enableAuth(requests[0].id, [AliceAddress])
// the ui should then move on to connecting to the rpcs
topMenuItems.multiproxySelector().should('be.visible')
})
})

it('Abort a tx with Alice', () => {
multisigPage.newTransactionButton().click()
sendTxModal.sendTxTitle().should('be.visible')
fillAndSubmitTransactionForm()
cy.getTxRequests().then((req) => {
const txRequests = Object.values(req)
console.log('txRequests', JSON.stringify(txRequests))
cy.wrap(txRequests.length).should('eq', 1)
cy.wrap(txRequests[0].payload.address).should('eq', AliceAddress)
sendTxModal.buttonSend().should('be.disabled')
const errorMessage = 'Whuuuut'
cy.rejectTx(txRequests[0].id, errorMessage)
notifications.errorNotificationIcon().should('be.visible')
notifications.notificationWrapper().should('have.length', 1)
notifications.notificationWrapper().should('contain', errorMessage)
})
})

it.skip('Makes a balance transfer with Alice', () => {
multisigPage.newTransactionButton().click()
sendTxModal.sendTxTitle().should('be.visible')
fillAndSubmitTransactionForm()
cy.getTxRequests().then((req) => {
const txRequests = Object.values(req)
cy.wrap(txRequests.length).should('eq', 1)
cy.wrap(txRequests[0].payload.address).should('eq', AliceAddress)
sendTxModal.buttonSend().should('be.disabled')
cy.approveTx(txRequests[0].id)
notifications.loadingNotificationIcon().should('be.visible')
notifications.notificationWrapper().should('have.length', 1)
notifications.notificationWrapper().should('contain', 'broadcast')
})
})
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { addresses } from '../../fixtures/accounts'
import { landingPageUrl, settingsPageWatchAccountUrl } from '../../fixtures/landingData'
import { landingPage } from '../../support/page-objects/landingPage'
import { settingsPage } from '../../support/page-objects/settingsPage'
import { addresses } from '../fixtures/accounts'
import { landingPageUrl, settingsPageWatchAccountUrl } from '../fixtures/landingData'
import { landingPage } from '../support/page-objects/landingPage'
import { settingsPage } from '../support/page-objects/settingsPage'

const addWatchAccount = (address: string, name?: string) => {
settingsPage.accountAddressInput().type(`${address}{enter}`, { delay: 20 })
Expand Down
8 changes: 0 additions & 8 deletions packages/ui/cypress/tsconfig.json

This file was deleted.

2 changes: 2 additions & 0 deletions packages/ui/src/components/EasySetup/BalancesTransfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,10 @@ const BalancesTransfer = ({ className, onSetExtrinsic, onSetErrorMessage, from }
allowAnyAddressInput={true}
onInputChange={onInputChange}
accountList={accountBase}
testId="field-to"
/>
<TextFieldStyled
data-cy="field-amount"
label={`Amount`}
onChange={onAmountChange}
value={amountString}
Expand Down
Loading

0 comments on commit 7bcb997

Please sign in to comment.