Test authenticated features
Learn how to efficiently test authenticated features in applications using Kinde authentication with Cypress. This guide covers reusing authentication state to test protected routes and features after authentication. Learn more about testing authenticated features with Kinde.
Cypress supports saving and reusing browser authentication state using cy.session(). This is the most efficient approach for testing authenticated features. By saving the authentication state once and reusing it across tests, you can significantly speed up your test suite while maintaining test isolation.
What you need
Link to this section- Completed the Cypress setup
- A test user and a running Kinde application.
Setup Cypress session
Link to this section-
Add the following to the
commands.jsfile:cypress/support/commands.js Cypress.Commands.add('login', () => {cy.session('user', () => {cy.visit(Cypress.env('TEST_APP_URL'))cy.get('[data-testid="sign-in-button"]').click()// Wait for Kinde login pagecy.url().should('include', 'kinde.com')cy.origin(Cypress.env('KINDE_ISSUER_URL'), () => {// Enter credentialscy.get('input[name="p_email"]').type(Cypress.env('TEST_USER_EMAIL'))cy.get('button[type="submit"]').click()cy.get('input[name="p_password"]').type(Cypress.env('TEST_USER_PASSWORD'))cy.get('button[type="submit"]').click({ multiple: true })})// Wait for redirect back to appconst appUrl = new URL(Cypress.env('TEST_APP_URL'))cy.url().should('include', appUrl.hostname)cy.get('[data-testid="user-profile"]').should('be.visible')})})
Create and run sample tests
Link to this sectionIf you are starting from scratch, you can run sample tests using the Kinde starter test application.
-
Create a new
dashboard.cy.jsfile:Terminal window touch cypress/e2e/dashboard.cy.js -
Add the following sample tests to the file:
cypress/e2e/dashboard.cy.js describe('Authenticated Features', () => {beforeEach(() => {// Restore session before each testcy.login()})// This test runs with pre-authenticated stateit('user can view dashboard', () => {cy.visit('/dashboard')// No login required - already authenticatedcy.get('[data-testid="start-hero-intro"]').should('contain.text', 'Woohoo!')})it('user can update profile', () => {cy.visit('/profile')cy.get('[data-testid="display-name-input"]').type('Test User')cy.get('[data-testid="display-name-submit"]').click()cy.get('.toast-success').should('be.visible')})}) -
Use the following command:
Terminal window npx cypress open -
Select the test spec to run.
You should now see the Cypress tests passing in the browser.
Complete project structure
Link to this sectionyour-project/├── cypress.config.js├── cypress.env.json # Environment variables (gitignored)├── cypress/│ ├── e2e/│ │ └── dashboard.cy.js # Tests using saved auth│ └── support/│ └── commands.js # Custom commands└── package.jsonCI/CD integration (GitHub Actions)
Link to this sectionname: E2E Tests
on: push: branches: [main] pull_request: branches: [main]
jobs: test: runs-on: ubuntu-latest
steps: - uses: actions/checkout@v4
- uses: actions/setup-node@v4 with: node-version: 20
- name: Install dependencies run: npm ci
- name: Install Cypress run: npm install cypress --save-dev
- name: Run Cypress tests run: npx cypress run env: TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }} TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }} TEST_APP_URL: ${{ secrets.TEST_APP_URL }} KINDE_ISSUER_URL: ${{ secrets.KINDE_ISSUER_URL }}
- uses: actions/upload-artifact@v4 if: always() with: name: cypress-screenshots path: cypress/screenshots retention-days: 30Troubleshooting
Link to this sectionTests fail with “Navigation timeout”
Link to this sectionKinde redirects can take time. Increase timeout:
cy.url({ timeout: 30000 }).should('include', 'your-app.com')Session not persisting
Link to this sectionEnsure cy.session() is called in beforeEach or at the start of each test. Check Cypress configuration for session storage settings. Verify that cypress/support/e2e.js is properly imported in your cypress.config.js.
Kinde form selectors changed
Link to this sectionKinde may update the UI. Use more resilient selectors:
// Instead of specific class namescy.origin(Cypress.env('KINDE_ISSUER_URL'), () => { cy.get('input[type="email"]').type(email) cy.get('input[type="password"]').type(password)})