Bonaventure OgetoBy Bonaventure Ogeto|

End to End Payment Tests with Cypress

Cypress cannot directly interact with cross-origin iframes by default. To test the Paystack popup, either: (1) use the cypress-iframe plugin (cy.frameLoaded() + cy.iframe()) to interact with iframe content, or (2) stub the Paystack API responses in your backend and mock the checkout callback. The second approach is more reliable for CI pipelines.

Cypress Test with cypress-iframe Plugin

// cypress/e2e/checkout.cy.js
// Requires: npm install -D cypress-iframe
// In cypress/support/commands.js: import 'cypress-iframe'
// In cypress.config.js: chromeWebSecurity: false

describe('Paystack checkout flow', () => {
  it('completes payment with test card', () => {
    cy.visit('/checkout?product=1');

    // Click pay button
    cy.get('[data-testid="pay-button"]').click();

    // Wait for Paystack iframe to load
    cy.frameLoaded('iframe[name="checkout"]');

    // Interact with inputs inside the iframe
    cy.iframe('iframe[name="checkout"]')
      .find('input[placeholder="Card Number"]')
      .type('4084084084084081');

    cy.iframe('iframe[name="checkout"]')
      .find('input[placeholder="MM/YY"]')
      .type('05/30');

    cy.iframe('iframe[name="checkout"]')
      .find('input[placeholder="CVV"]')
      .type('408');

    cy.iframe('iframe[name="checkout"]')
      .contains('button', 'Pay')
      .click();

    // Assert redirect to success page
    cy.url({ timeout: 30000 }).should('include', '/payment/success');
    cy.contains('Payment Successful').should('be.visible');
  });
});

// Alternative: stub approach (more reliable in CI)
describe('Paystack checkout — stubbed approach', () => {
  it('handles successful payment callback', () => {
    cy.intercept('POST', '/api/checkout', {
      statusCode: 200,
      body: { authorization_url: 'https://checkout.paystack.com/test', reference: 'ref_001' },
    }).as('initCheckout');

    cy.visit('/checkout?product=1');
    cy.get('[data-testid="pay-button"]').click();

    cy.wait('@initCheckout');

    // Simulate the Paystack callback being called (as if payment succeeded)
    cy.window().then((win) => {
      win.PaystackPop.setup({ callback: (response) => win.handlePaystackCallback(response) });
      win.handlePaystackCallback({ reference: 'ref_001', status: 'success' });
    });

    cy.url({ timeout: 10000 }).should('include', '/payment/success');
  });
});

Learn More

Key Takeaways

  • Cypress blocks cross-origin iframes by default — set chromeWebSecurity: false in cypress.config.js for local testing with Paystack popup.
  • Use the cypress-iframe plugin for clean iframe interactions: cy.frameLoaded("[name='checkout']") then cy.iframe().find("input").type(...).
  • An alternative to iframe interaction: stub your backend API responses and simulate the post-payment callback directly.
  • Always use Paystack test mode credentials — never run Cypress tests against live Paystack keys.
  • Use cy.intercept() to verify your frontend makes the correct request to your checkout endpoint.
  • E2E tests should verify the database state after payment — add a cy.task() to query the DB and assert the order is marked paid.

Frequently Asked Questions

Why does Cypress say "This iframe is cross-origin and cannot be accessed"?
Cypress enforces same-origin policy by default. Setting chromeWebSecurity: false in cypress.config.js disables this check, allowing iframe interaction. This is fine for local testing but should not be needed in production — the cross-origin restriction is a browser security feature, not a bug.
Is the stubbed approach or the real iframe approach better for Paystack tests?
The stubbed approach (intercepting your backend checkout endpoint) is faster and more reliable in CI — it does not depend on Paystack's popup UI rendering correctly. The real iframe approach tests more of the actual user flow but is slower and can fail if Paystack changes their popup structure. Use the real approach for a small set of smoke tests and the stubbed approach for broader coverage.
How do I add a cy.task() to verify database state after payment?
In cypress.config.js, add a task: on("task", { checkOrderStatus: async (orderId) => { const order = await db.orders.findByPk(orderId); return order.status; } }). In your test: cy.task("checkOrderStatus", orderId).should("eq", "paid"). This runs database queries in the Node.js environment outside the browser.
Can Cypress tests run in GitHub Actions?
Yes. Use the official cypress-io/github-action GitHub Action or install Cypress manually (npm ci && npx cypress install). Run with --headless --browser chrome. Ensure your test environment variables (PAYSTACK_SECRET_KEY=sk_test_...) are set as GitHub Actions secrets.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon