End-to-end (E2E) testing is crucial for ensuring that web applications function correctly from the user's perspective. Qwik, a modern framework focused on performance and instant on-demand loading, offers unique challenges and opportunities for E2E testing. In this article, we explore real-world examples of Qwik E2E tests, focusing on handling authentication flows and managing application state effectively.

Setting Up the Testing Environment

Before diving into specific test cases, it’s important to establish a robust testing environment. Common tools include Cypress, Playwright, or Selenium. For Qwik applications, Playwright is often preferred due to its modern API and support for multiple browsers. Ensure your testing setup can handle asynchronous operations, route changes, and complex UI interactions.

Example 1: Testing Authentication Flow

Authenticating users typically involves login forms, token management, and protected routes. Here's a simplified example of testing a login process in a Qwik app using Playwright.

Test: Successful Login

In this test, we simulate a user entering credentials and verify redirection to a protected dashboard.

import { test, expect } from '@playwright/test';

test('User can login successfully', async ({ page }) => {
  await page.goto('https://your-qwik-app.com/login');

  await page.fill('input[name="username"]', 'testuser');
  await page.fill('input[name="password"]', 'password123');

  await page.click('button[type="submit"]');

  await expect(page).toHaveURL('https://your-qwik-app.com/dashboard');

  const welcomeMessage = await page.locator('text=Welcome, testuser');
  await expect(welcomeMessage).toBeVisible();
});

Test: Handling Authentication Failure

We also test invalid login attempts to ensure proper error handling.

test('Displays error on failed login', async ({ page }) => {
  await page.goto('https://your-qwik-app.com/login');

  await page.fill('input[name="username"]', 'wronguser');
  await page.fill('input[name="password"]', 'wrongpass');

  await page.click('button[type="submit"]');

  const errorMsg = await page.locator('text=Invalid credentials');
  await expect(errorMsg).toBeVisible();
});

Example 2: Managing Application State

Qwik's reactivity and lazy loading require tests that verify state persistence and updates. Below is an example of testing a counter component that updates state on user interaction.

Test: Counter Increment

This test simulates clicking a button and checks if the counter updates accordingly.

import { test, expect } from '@playwright/test';

test('Counter increments on click', async ({ page }) => {
  await page.goto('https://your-qwik-app.com/counter');

  const counter = page.locator('data-testid=counter-value');
  const incrementButton = page.locator('data-testid=increment');

  await expect(counter).toHaveText('0');

  await incrementButton.click();

  await expect(counter).toHaveText('1');

  await incrementButton.click();

  await expect(counter).toHaveText('2');
});

Test: State Persistence After Reload

To verify that state persists after page reloads, simulate a refresh and check the counter value.

test('Counter state persists after reload', async ({ page }) => {
  await page.goto('https://your-qwik-app.com/counter');

  const incrementButton = page.locator('data-testid=increment');

  await incrementButton.click();
  await incrementButton.click();

  await page.reload();

  const counter = page.locator('data-testid=counter-value');
  await expect(counter).toHaveText('2');
});

Best Practices for Qwik E2E Testing

  • Mock external APIs and services to isolate tests.
  • Use data-testid attributes for reliable element selection.
  • Test critical user flows thoroughly, including edge cases.
  • Ensure tests are idempotent and can run repeatedly without side effects.
  • Leverage Qwik's lazy loading to test component-specific behaviors.

By implementing comprehensive E2E tests for authentication and state management, developers can significantly improve the reliability and user experience of Qwik applications. Continuous testing ensures that new features do not break existing functionality and that the app remains robust under various scenarios.