Skip to content

Commit

Permalink
feat: Add test suite for check the Safe Apps liveness (#447)
Browse files Browse the repository at this point in the history
* Add liveness test for safe apps

* Use chrome
  • Loading branch information
yagopv authored May 25, 2022
1 parent 9315b20 commit 4b1ae98
Show file tree
Hide file tree
Showing 11 changed files with 762 additions and 15 deletions.
6 changes: 5 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ module.exports = {
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }],
},
};
globals: {
cy: 'readonly',
Cypress: 'readonly',
},
}
43 changes: 43 additions & 0 deletions .github/workflows/safe-apps-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Test Safe Apps
on:
workflow_dispatch:
inputs:
baseUrl:
description: 'Safe Web URL'
required: true
default: 'http://gnosis-safe.io/app'
networkPrefix:
description: 'Address prefix (eth,rin...)'
required: true
default: 'eth'
safeAddress:
description: 'Safe address'
required: true
default: '0xc322cde085A4C6EFc865E77eDDdF39c31262Fc70'
configServiceBaseUrl:
description: 'Config service base URL'
required: true
default: 'https://safe-client.gnosis.io'
schedule:
# At 9:00 on every day-of-week from Monday through Friday
- cron: '0 9 * * 1-5'

jobs:
e2e:
runs-on: ubuntu-20.04
# let's make sure our tests pass on Chrome browser
name: E2E on Chrome
steps:
- uses: actions/checkout@v2
- uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/safe-apps-check.spec.js
env:
CI: 'true'
CYPRESS_BASE_URL: ${{ github.event.inputs.baseUrl || 'http://gnosis-safe.io/app' }}
CYPRESS_NETWORK_PREFIX: ${{ github.event.inputs.networkPrefix || 'eth' }}
CYPRESS_TESTING_SAFE_ADDRESS: ${{ github.event.inputs.safeAddress || '0xc322cde085A4C6EFc865E77eDDdF39c31262Fc70' }}
CYPRESS_CONFIG_SERVICE_BASE_URL: ${{ github.event.inputs.configServiceBaseUrl || 'https://safe-client.gnosis.io' }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
continue-on-error: true
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ yarn-error.log*

# vscode folders
.vscode

cypress/reports
cypress/videos
cypress/screenshots
8 changes: 8 additions & 0 deletions cypress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"chromeWebSecurity": false,
"video": false,
"retries": {
"runMode": 2,
"openMode": 0
}
}
23 changes: 23 additions & 0 deletions cypress/integration/safe-apps-check.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const safeAppsList = Cypress.env('SAFE_APPS_LIST') || []

describe('Safe Apps List', () => {
before(() => {
expect(safeAppsList).to.be.an('array').and.to.have.length.greaterThan(0)
})

safeAppsList.forEach(safeApp => {
it(safeApp.name, () => {
cy.visit(
`${Cypress.env('BASE_URL')}/${Cypress.env('NETWORK_PREFIX')}:${Cypress.env(
'TESTING_SAFE_ADDRESS',
)}/apps?appUrl=${safeApp.url}`,
)
const iframeSelector = `iframe[id="iframe-${safeApp.url}"]`

cy.findByText('Accept all').click({ force: true })
cy.findByText('Confirm').click({ force: true })
cy.frameLoaded(iframeSelector)
cy.iframe(iframeSelector).get('#root,#app,.app,main,#__next,app-root,#___gatsby')
})
})
})
106 changes: 106 additions & 0 deletions cypress/lib/slack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const axios = require('axios')

export const sendSlackMessage = async results => {
if (results) {
try {
const url = process.env.SLACK_WEBHOOK_URL
if (!url) {
return
}

await axios.post(process.env.SLACK_WEBHOOK_URL, buildSlackMessage(results))
} catch (error) {
console.error(error)
}
}
}

const buildSlackMessage = results => {
const failedTests = results.runs[0].tests
.filter(test => test.state === 'failed')
.map(test => test.title[1])

const title = {
type: 'section',
text: {
type: 'mrkdwn',
text: '*Safe Apps liveness tests*',
},
}

const executionEnvironment = {
type: 'section',
fields: [
{
type: 'mrkdwn',
text: `*Domain:*\n${process.env.CYPRESS_BASE_URL}`,
},
{
type: 'mrkdwn',
text: `*Network:*\n${process.env.CYPRESS_NETWORK_PREFIX}`,
},
{
type: 'mrkdwn',
text: `*Safe Address:*\n${process.env.CYPRESS_TESTING_SAFE_ADDRESS}`,
},
{
type: 'mrkdwn',
text: `*Config Service:*\n${process.env.CYPRESS_CONFIG_SERVICE_BASE_URL}`,
},
],
}

const safeUrl = {
type: 'section',
text: {
type: 'mrkdwn',
text: `*Safe URL:*\n${process.env.CYPRESS_BASE_URL}/${process.env.CYPRESS_NETWORK_PREFIX}:${process.env.CYPRESS_TESTING_SAFE_ADDRESS}/apps`,
},
}

const executionResult = {
type: 'section',
text: {
type: 'mrkdwn',
text: `*Execution results:*\n${results.totalPassed} out of ${results.totalTests}, passed`,
},
}

const failingApps = {
type: 'section',
text: {
type: 'mrkdwn',
text: `*Failing Apps:* _${failedTests.toString()}_`,
},
}

const action = {
type: 'section',
text: {
type: 'mrkdwn',
text: 'Want to take a look to the execution ?',
},
accessory: {
type: 'button',
text: {
type: 'plain_text',
text: 'Take me there !',
emoji: true,
},
value: 'click_me_123',
url: 'https://github.com/safe-global/safe-react-apps/actions/workflows/safe-apps-check.yml',
action_id: 'button-action',
},
}

const blocks = [title, executionEnvironment, safeUrl, executionResult]

if (failedTests.length > 0) {
blocks.push(failingApps)
}
blocks.push(action)

return {
blocks,
}
}
27 changes: 27 additions & 0 deletions cypress/plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const axios = require('axios')
const { sendSlackMessage } = require('../lib/slack')

require('dotenv').config()

module.exports = async (on, config) => {
on('after:run', sendSlackMessage)

let safeAppsList

try {
safeAppsList = await axios.get(
`${
process.env.CYPRESS_CONFIG_SERVICE_BASE_URL
}/v1/chains/1/safe-apps?client_url=${encodeURIComponent(process.env.BASE_URL)}`,
)
} catch (e) {
console.log('Unable to fetch the default list: ', e)
}

config.env.BASE_URL = process.env.CYPRESS_BASE_URL
config.env.SAFE_APPS_LIST = safeAppsList.data
config.env.NETWORK_PREFIX = process.env.CYPRESS_NETWORK_PREFIX
config.env.TESTING_SAFE_ADDRESS = process.env.CYPRESS_TESTING_SAFE_ADDRESS

return config
}
153 changes: 153 additions & 0 deletions cypress/support/iframe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
const DEFAULT_OPTS = {
log: true,
timeout: 30000,
}

const DEFAULT_IFRAME_SELECTOR = 'iframe'

function sleep(timeout) {
return new Promise(resolve => setTimeout(resolve, timeout))
}

// This command checks that an iframe has loaded onto the page
// - This will verify that the iframe is loaded to any page other than 'about:blank'
// cy.frameLoaded()

// - This will verify that the iframe is loaded to any url containing the given path part
// cy.frameLoaded({ url: 'https://google.com' })
// cy.frameLoaded({ url: '/join' })
// cy.frameLoaded({ url: '?some=query' })
// cy.frameLoaded({ url: '#/hash/path' })

// - You can also give it a selector to check that a specific iframe has loaded
// cy.frameLoaded('#my-frame')
// cy.frameLoaded('#my-frame', { url: '/join' })
Cypress.Commands.add('frameLoaded', (selector, opts) => {
if (selector === undefined) {
selector = DEFAULT_IFRAME_SELECTOR
} else if (typeof selector === 'object') {
opts = selector
selector = DEFAULT_IFRAME_SELECTOR
}

const fullOpts = {
...DEFAULT_OPTS,
...opts,
}
const log = fullOpts.log
? Cypress.log({
name: 'frame loaded',
displayName: 'frame loaded',
message: [selector],
}).snapshot()
: null
return cy.get(selector, { log: false }).then({ timeout: fullOpts.timeout }, async $frame => {
log?.set('$el', $frame)
if ($frame.length !== 1) {
throw new Error(
`cypress-iframe commands can only be applied to exactly one iframe at a time. Instead found ${$frame.length}`,
)
}

const contentWindow = $frame.prop('contentWindow')
const hasNavigated = fullOpts.url
? () =>
typeof fullOpts.url === 'string'
? contentWindow.location.toString().includes(fullOpts.url)
: fullOpts.url?.test(contentWindow.location.toString())
: () => contentWindow.location.toString() !== 'about:blank'

while (!hasNavigated()) {
await sleep(100)
}

if (contentWindow.document.readyState === 'complete') {
return $frame
}

const loadLog = Cypress.log({
name: 'Frame Load',
message: [contentWindow.location.toString()],
event: true,
}).snapshot()
await new Promise(resolve => {
Cypress.$(contentWindow).on('load', resolve)
})
loadLog.end()
log?.finish()
return $frame
})
})

// This will cause subsequent commands to be executed inside of the given iframe
// - This will verify that the iframe is loaded to any page other than 'about:blank'
// cy.iframe().find('.some-button').should('be.visible').click()
// cy.iframe().contains('Some hidden element').should('not.be.visible')
// cy.find('#outside-iframe').click() // this will be executed outside the iframe

// - You can also give it a selector to find elements inside of a specific iframe
// cy.iframe('#my-frame').find('.some-button').should('be.visible').click()
// cy.iframe('#my-second-frame').contains('Some hidden element').should('not.be.visible')
Cypress.Commands.add('iframe', (selector, opts) => {
if (selector === undefined) {
selector = DEFAULT_IFRAME_SELECTOR
} else if (typeof selector === 'object') {
opts = selector
selector = DEFAULT_IFRAME_SELECTOR
}

const fullOpts = {
...DEFAULT_OPTS,
...opts,
}
const log = fullOpts.log
? Cypress.log({
name: 'iframe',
displayName: 'iframe',
message: [selector],
}).snapshot()
: null
return cy.frameLoaded(selector, { ...fullOpts, log: false }).then($frame => {
log?.set('$el', $frame).end()
const contentWindow = $frame.prop('contentWindow')
return Cypress.$(contentWindow.document.body)
})
})

// This can be used to execute a group of commands within an iframe
// - This will verify that the iframe is loaded to any page other than 'about:blank'
// cy.enter().then(getBody => {
// getBody().find('.some-button').should('be.visible').click()
// getBody().contains('Some hidden element').should('not.be.visible')
// })
// - You can also give it a selector to find elements inside of a specific iframe
// cy.enter('#my-iframe').then(getBody => {
// getBody().find('.some-button').should('be.visible').click()
// getBody().contains('Some hidden element').should('not.be.visible')
// })
Cypress.Commands.add('enter', (selector, opts) => {
if (selector === undefined) {
selector = DEFAULT_IFRAME_SELECTOR
} else if (typeof selector === 'object') {
opts = selector
selector = DEFAULT_IFRAME_SELECTOR
}

const fullOpts = {
...DEFAULT_OPTS,
...opts,
}

const log = fullOpts.log
? Cypress.log({
name: 'enter',
displayName: 'enter',
message: [selector],
}).snapshot()
: null

return cy.iframe(selector, { ...fullOpts, log: false }).then($body => {
log?.set('$el', $body).end()
return () => cy.wrap($body, { log: false })
})
})
2 changes: 2 additions & 0 deletions cypress/support/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import '@testing-library/cypress/add-commands'
import './iframe'
Loading

0 comments on commit 4b1ae98

Please sign in to comment.