Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
name: E2E Test Suite

on:
pull_request:
branches:
- main
- test-setup
workflow_dispatch:

jobs:
e2e-tests:
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Enable Corepack
run: corepack enable

- name: Prepare Yarn 4.1.1
run: corepack prepare yarn@4.1.1 --activate

- name: Install dependencies
run: yarn install --immutable

- name: Verify Cypress installation
run: npx cypress verify

- name: Setup Chopsticks config
run: |
cat > config/kusama.yml << 'EOF'
endpoint: wss://rpc.ibp.network/kusama
mock-signature-host: true
db: ./db.sqlite
runtime-log-level: 0
EOF

- name: Start Chopsticks (background)
run: |
echo "Starting Chopsticks blockchain fork..."
nohup yarn chopsticks > chopsticks.log 2>&1 &
CHOPSTICKS_PID=$!
echo "CHOPSTICKS_PID=$CHOPSTICKS_PID" >> $GITHUB_ENV
echo "Chopsticks started with PID: $CHOPSTICKS_PID"
sleep 5
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious: why such a long sleep here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of failures due to timeout, even with only "sleep 3."

echo "Chopsticks output:"
cat chopsticks.log || true

- name: Wait for Chopsticks to be ready
run: |
echo "Waiting for Chopsticks to respond on http://localhost:8000..."
timeout 120 bash -c 'until curl -sf http://localhost:8000 > /dev/null 2>&1; do sleep 2; done' || {
echo "Chopsticks did not start within 120 seconds"
echo "Chopsticks log:"
cat chopsticks.log || true
exit 1
}
echo "✓ Chopsticks is running and responding"

- name: Start dev server (background)
run: |
echo "Starting React dev server..."
nohup yarn start > dev-server.log 2>&1 &
DEV_SERVER_PID=$!
echo "DEV_SERVER_PID=$DEV_SERVER_PID" >> $GITHUB_ENV
echo "Dev server started with PID: $DEV_SERVER_PID"
env:
NODE_OPTIONS: --openssl-legacy-provider
CI: true

- name: Wait for dev server to be ready
run: |
echo "Waiting for dev server to respond on http://localhost:3000..."
timeout 120 bash -c 'until curl -sf http://localhost:3000 > /dev/null 2>&1; do sleep 3; done' || {
echo "Dev server did not start within 120 seconds"
exit 1
}
echo "✓ Dev server is running and responding"

- name: Run Cypress E2E tests
run: |
echo "Running E2E test suite..."
npx cypress run --config video=true --reporter json --reporter-options "output=cypress/results/results.json"

- name: Upload test artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: cypress-artifacts
path: |
cypress/screenshots/
cypress/videos/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove videos and add the chopsticks logs to the artifacts

cypress/results/
retention-days: 7

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: cypress/results/
retention-days: 30

- name: Generate test summary
if: always()
run: |
echo "## E2E Test Results" >> $GITHUB_STEP_SUMMARY
if [ -f cypress/results/results.json ]; then
echo "✓ Tests completed - see artifacts for details" >> $GITHUB_STEP_SUMMARY
else
echo "⚠ No test results generated" >> $GITHUB_STEP_SUMMARY
fi

- name: Cleanup
if: always()
run: |
echo "Cleaning up background processes..."
if [ ! -z "$CHOPSTICKS_PID" ]; then
kill $CHOPSTICKS_PID 2>/dev/null || true
fi
if [ ! -z "$DEV_SERVER_PID" ]; then
kill $DEV_SERVER_PID 2>/dev/null || true
fi
32 changes: 32 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { defineConfig } from 'cypress';

export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
setupNodeEvents(on, config) {
return config;
},
specPattern: 'cypress/e2e/*.cy.{js,jsx,ts,tsx}',
supportFile: 'cypress/support/e2e.ts',

viewportWidth: 1280,
viewportHeight: 720,

video: false,
screenshotOnRunFailure: true,
screenshotsFolder: 'cypress/screenshots',

retries: {
runMode: 2,
openMode: 0,
},

defaultCommandTimeout: 10000,
requestTimeout: 15000,
responseTimeout: 15000,

env: {
chopsticks_url: 'ws://localhost:8000',
},
},
});
3 changes: 0 additions & 3 deletions cypress.json

This file was deleted.

227 changes: 227 additions & 0 deletions cypress/e2e/smoke.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
describe('Primary Routes Smoke Tests', () => {
const primaryRoutes = [
{ path: '/', name: 'Landing Page', hasCustomNav: true },
{ path: '/welcome', name: 'Welcome Page', hasCustomNav: false },
{ path: '/journey', name: 'Journey Page', hasCustomNav: false },
{ path: '/guide', name: 'Cyborg Guide Page', hasCustomNav: false },
{ path: '/wiki', name: 'Wiki Page', hasCustomNav: false },
{ path: '/gilbertogil', name: 'Gilberto Gil Page', hasCustomNav: false },
{ path: '/futurivel', name: 'Futurivel Page', hasCustomNav: false }
]

primaryRoutes.forEach(({ path, name, hasCustomNav }) => {
it(`should load ${name} (${path})`, () => {
cy.visit(path, { timeout: 5000 })

cy.get('body').should('be.visible')

if (!hasCustomNav) {
cy.get('nav').should('exist')
}

cy.window().then((win) => {
expect(win.document.readyState).to.equal('complete')
})
})
})

it('should render all primary routes without crashes', () => {
cy.visit('/')
cy.get('body').should('be.visible')

cy.visit('/welcome')
cy.get('body').should('be.visible')

cy.visit('/journey')
cy.get('body').should('be.visible')

cy.visit('/guide')
cy.get('body').should('be.visible')

cy.visit('/wiki')
cy.get('body').should('be.visible')

cy.visit('/gilbertogil')
cy.get('body').should('be.visible')

cy.visit('/futurivel')
cy.get('body').should('be.visible')
})
})

describe('Explore Routes Smoke Tests', () => {
const exploreRoutes = [
{ path: '/explore/bidders', name: 'Bidders' },
{ path: '/explore/candidates', name: 'Candidates' },
{ path: '/explore/members', name: 'Members' },
{ path: '/explore/payouts', name: 'Payouts' },
{ path: '/explore/suspended', name: 'Suspended' }
]

beforeEach(() => {
cy.visit('/')
})

exploreRoutes.forEach(({ path, name }) => {
it(`should load ${name} page (${path})`, () => {
cy.visit(`${path}?rpc=ws://localhost:8000`)

cy.get('body').should('be.visible')
cy.get('nav').should('exist')
cy.url().should('include', path)
})
})

it('should redirect /explore to /explore/bidders', () => {
cy.visit('/explore?rpc=ws://localhost:8000')

cy.url().should('include', '/explore/bidders')
cy.get('body').should('be.visible')
})

it('should navigate between explore sub-routes', () => {
cy.visit('/explore/bidders?rpc=ws://localhost:8000')
cy.url().should('include', '/explore/bidders')

cy.visit('/explore/members?rpc=ws://localhost:8000')
cy.url().should('include', '/explore/members')
cy.get('body').should('be.visible')

cy.visit('/explore/candidates?rpc=ws://localhost:8000')
cy.url().should('include', '/explore/candidates')
cy.get('body').should('be.visible')
})

it('should maintain query parameter during navigation', () => {
cy.visit('/explore/bidders?rpc=ws://localhost:8000')

cy.url().should('include', 'rpc=ws://localhost:8000')
})
})

describe('Proof of Ink Routes Smoke Tests', () => {
const poiRoutes = [
{ path: '/explore/poi/examples', name: 'POI Examples' },
{ path: '/explore/poi/rules', name: 'POI Rules' },
{ path: '/explore/poi/gallery', name: 'POI Gallery' },
{ path: '/explore/poi/next-head', name: 'POI Next Head' }
]

beforeEach(() => {
cy.visit('/?rpc=ws://localhost:8000')
})

poiRoutes.forEach(({ path, name }) => {
it(`should load ${name} page (${path})`, () => {
cy.visit(`${path}?rpc=ws://localhost:8000`)

cy.get('body').should('be.visible')
cy.get('nav').should('exist')
cy.url().should('include', path)
})
})

it('should redirect /explore/poi to /explore/poi/examples', () => {
cy.visit('/explore/poi?rpc=ws://localhost:8000')

cy.url().should('include', '/explore/poi/examples')
cy.get('body').should('be.visible')
})

it('should navigate between POI sub-routes', () => {
cy.visit('/explore/poi/examples?rpc=ws://localhost:8000')
cy.url().should('include', '/explore/poi/examples')
cy.get('body').should('be.visible')

cy.visit('/explore/poi/rules?rpc=ws://localhost:8000')
cy.url().should('include', '/explore/poi/rules')
cy.get('body').should('be.visible')

cy.visit('/explore/poi/gallery?rpc=ws://localhost:8000')
cy.url().should('include', '/explore/poi/gallery')
cy.get('body').should('be.visible')

cy.visit('/explore/poi/next-head?rpc=ws://localhost:8000')
cy.url().should('include', '/explore/poi/next-head')
cy.get('body').should('be.visible')
})

it('should maintain query parameter in POI routes', () => {
cy.visit('/explore/poi/examples?rpc=ws://localhost:8000')

cy.url().should('include', 'rpc=ws://localhost:8000')

cy.visit('/explore/poi/gallery?rpc=ws://localhost:8000')
cy.url().should('include', 'rpc=ws://localhost:8000')
})
})

describe('API Connection Smoke Tests', () => {
it('should connect to Chopsticks successfully', () => {
cy.visit('/explore/members?rpc=ws://localhost:8000')

cy.get('body').should('be.visible')

cy.get('body').then(($body) => {
const bodyText = $body.text()

expect(bodyText.length).to.be.greaterThan(10)
expect(bodyText).to.not.equal('Loading...')
})

cy.get('body').should('not.contain', 'Failed to connect')
cy.get('body').should('not.contain', 'API error')
})

it('should display chain information', () => {
cy.visit('/explore/members?rpc=ws://localhost:8000')

cy.get('body').should('be.visible')
cy.get('nav').should('exist')
cy.url().should('include', 'explore')
})

it('should initialize API without crashing', () => {
cy.visit('/explore/members?rpc=ws://localhost:8000')

cy.wait(5000)

cy.get('body').should('be.visible')
cy.get('nav').should('exist')
})
})

describe('Query Parameter Preservation Smoke Tests', () => {
const testRpc = 'ws://localhost:8000'

it('should preserve ?rpc parameter on initial load', () => {
cy.visit(`/?rpc=${testRpc}`)

cy.url().should('include', `rpc=${testRpc}`)
cy.get('body').should('be.visible')
})

it('should preserve ?rpc parameter through redirects', () => {
cy.visit(`/explore?rpc=${testRpc}`)

cy.url().should('include', '/explore/bidders')
cy.url().should('include', `rpc=${testRpc}`)

cy.visit(`/explore/poi?rpc=${testRpc}`)

cy.url().should('include', '/explore/poi/examples')
cy.url().should('include', `rpc=${testRpc}`)
})

it('should preserve params in deeply nested routes', () => {
cy.visit(`/explore/poi/gallery?rpc=${testRpc}`)

cy.url().should('include', '/explore/poi/gallery')
cy.url().should('include', `rpc=${testRpc}`)

cy.visit(`/explore/poi/rules?rpc=${testRpc}`)

cy.url().should('include', '/explore/poi/rules')
cy.url().should('include', `rpc=${testRpc}`)
})
})
Loading
Loading