Why E2E Testing Matters for Shopify

A checkout bug that costs 0.1% of orders sounds small. For a $10M store, that's $10K in lost revenue annually. For a $50M store, it's $50K. Scale this across 10 bugs and you're hemorrhaging six figures.

Most Shopify teams don't do automated E2E testing. They rely on manual QA—someone clicking through checkout before a launch. This catches 60% of issues. The other 40% slip to production.

Playwright changes this. It's an open-source testing framework by Microsoft that automates browser interactions. You write tests that simulate real user behavior—add a product to cart, enter shipping info, complete payment. Run these tests in your CI/CD pipeline before every deployment.

Result: bugs get caught before production. You ship with confidence.

What Is Playwright?

Playwright is a framework for automating browser interactions. It can control Chrome, Firefox, Safari, and Edge. You write tests in JavaScript/TypeScript that:

  • Navigate pages
  • Click buttons
  • Fill forms
  • Assert text, images, and elements
  • Wait for dynamic content to load

Here's a simple example:

test('Add product to cart and proceed to checkout', async ({ page }) => {
  await page.goto('https://yourstore.myshopify.com/products/hoodie');
  await page.click('button:has-text("Add to cart")');
  await page.goto('https://yourstore.myshopify.com/cart');
  expect(await page.locator('p:has-text("Hoodie")').count()).toBe(1);
  await page.click('a:has-text("Proceed to Checkout")');
  expect(page.url()).toContain('/checkout');
});

This test:

  1. Visits a product page
  2. Clicks "Add to cart"
  3. Navigates to the cart
  4. Verifies the product is in the cart
  5. Proceeds to checkout
  6. Confirms the URL changed to /checkout

Run this test after every code change. If checkout breaks, the test fails and alerts your team before you deploy.

Playwright vs. Selenium vs. Cypress

Framework Language Browser Support Speed Learning Curve Best For
Playwright JS/TypeScript/Python/Java Chrome, Firefox, Safari, Edge Fast (4+ pages/sec) Medium All testing scenarios
Selenium Multiple languages All browsers Slow (1–2 pages/sec) High Legacy projects, multi-browser
Cypress JS/TypeScript Chrome, Firefox, Edge Fast Low Modern web app testing

Playwright is faster than Selenium, has better multi-browser support than Cypress, and works in multiple languages. For Shopify, it's the gold standard.

Setting Up Playwright for Shopify

Step 1: Install Playwright

npm init -y
npm install @playwright/test --save-dev
npx playwright install

Step 2: Create a Test Directory Structure

your-project/
├── tests/
│   ├── checkout.spec.ts
│   ├── product-page.spec.ts
│   ├── cart.spec.ts
│   ├── search.spec.ts
│   └── filters.spec.ts
├── playwright.config.ts
└── package.json

Step 3: Configure Playwright (playwright.config.ts)

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  
  use: {
    baseURL: 'https://yourstore.myshopify.com',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },

  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

This configures Playwright to:

  • Run tests against your Shopify store
  • Test in Chrome, Firefox, and Safari
  • Take screenshots on failure
  • Run in parallel for speed
  • Retry failed tests twice in CI

Five Critical Tests for Every Shopify Store

Test 1: Product Page — Add to Cart

test('Product page: Add to cart and verify cart count', async ({ page }) => {
  await page.goto('/products/sample-product');
  await page.waitForSelector('button[aria-label*="Add to cart"]');
  await page.click('button[aria-label*="Add to cart"]');
  const cartCount = await page.locator('[data-cart-count]');
  await expect(cartCount).toContainText('1');
});

This test verifies:

  • Product page loads
  • "Add to cart" button works
  • Cart count updates

Test 2: Checkout Flow — Guest Checkout

test('Checkout: Guest checkout with email, shipping, and payment', async ({ page }) => {
  // Add product to cart
  await page.goto('/products/sample-product');
  await page.click('button:has-text("Add to cart")');
  
  // Go to checkout
  await page.goto('/cart');
  await page.click('a:has-text("Proceed to checkout")');
  
  // Fill email
  await page.fill('input[type="email"]', '[email protected]');
  
  // Fill shipping
  await page.fill('input[name="firstName"]', 'John');
  await page.fill('input[name="lastName"]', 'Doe');
  await page.fill('input[name="address1"]', '123 Main St');
  await page.fill('input[name="city"]', 'Portland');
  await page.selectOption('select[name="province"]', 'OR');
  await page.fill('input[name="postalCode"]', '97201');
  await page.fill('input[name="phone"]', '5035551234');
  
  // Select shipping method
  await page.click('input[value="standard"]');
  
  // Proceed to payment
  await page.click('button:has-text("Continue to payment")');
  
  // Verify payment form loads
  await expect(page).toHaveURL(/.*checkout.*payment/);
});

This test:

  • Adds a product to cart
  • Navigates to checkout
  • Fills email, name, address
  • Selects shipping method
  • Verifies payment form loads

Test 3: Search and Filter

test('Collection page: Filter by price and verify results', async ({ page }) => {
  await page.goto('/collections/products');
  
  // Click price filter
  await page.click('label:has-text("$0 – $50")');
  
  // Wait for products to update
  await page.waitForFunction(() => {
    const products = document.querySelectorAll('[data-product-item]');
    return products.length > 0;
  });
  
  // Verify all products are under $50
  const products = await page.locator('[data-product-item]').all();
  expect(products.length).toBeGreaterThan(0);
});

This test:

  • Navigates to collection
  • Applies price filter
  • Waits for products to load
  • Verifies results exist

Test 4: Cart Manipulation

test('Cart: Update quantity and remove items', async ({ page }) => {
  // Add two products
  await page.goto('/products/product-1');
  await page.click('button:has-text("Add to cart")');
  await page.goto('/products/product-2');
  await page.click('button:has-text("Add to cart")');
  
  // Go to cart
  await page.goto('/cart');
  
  // Update quantity
  await page.fill('input[name="quantity"]', '5');
  
  // Verify total updated
  const total = await page.locator('[data-subtotal]');
  await expect(total).not.toContainText('$0.00');
  
  // Remove item
  await page.click('button[data-remove-item]');
  
  // Verify only one item remains
  const itemCount = await page.locator('[data-cart-item]').count();
  expect(itemCount).toBe(1);
});

This test:

  • Adds multiple products
  • Updates quantity
  • Removes items
  • Verifies cart state changes

Test 5: Mobile Responsiveness

test('Mobile: Product page loads and add-to-cart works on mobile', async ({ page }) => {
  // Set mobile viewport
  await page.setViewportSize({ width: 375, height: 667 });
  
  // Navigate to product
  await page.goto('/products/sample-product');
  
  // Verify product image is visible
  await expect(page.locator('img[alt*="product"]')).toBeVisible();
  
  // Verify add-to-cart button is tappable (size >= 44x44px)
  const button = page.locator('button:has-text("Add to cart")');
  const box = await button.boundingBox();
  expect(box.width).toBeGreaterThan(44);
  expect(box.height).toBeGreaterThan(44);
});

This test:

  • Sets mobile viewport size
  • Verifies images load on mobile
  • Checks that buttons are tap-able

Running Tests Locally

# Run all tests
npx playwright test

# Run specific test file
npx playwright test tests/checkout.spec.ts

# Run tests in headed mode (see browser)
npx playwright test --headed

# Run single test
npx playwright test -g "Add to cart"

# Open test report
npx playwright show-report

Integrating Playwright into CI/CD (GitHub Actions)

Create a file .github/workflows/e2e-tests.yml:

name: E2E Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Install Playwright browsers
        run: npx playwright install --with-deps
      
      - name: Run E2E tests
        run: npx playwright test
      
      - name: Upload test report
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report
          path: playwright-report/

Now, every time you push code or open a pull request, the tests run automatically. If tests fail, the PR is blocked from merging. You can't ship broken checkout.

Common Playwright Patterns for Shopify

Wait for dynamic content (AJAX-loaded products):

await page.waitForSelector('[data-product-item]:nth-child(10)');

Handle Shopify Liquid-rendered attributes:

// Locate product by data attribute
const product = page.locator('[data-product-id="1234567890"]');

// Click variant selector
await page.selectOption('select[name="options"]', 'Blue');

Test with multiple currencies/locales:

test('Checkout in CAD', async ({ page }) => {
  // Set locale
  await page.goto('/?locale=en-CA');
  
  // Verify price shows CAD
  await expect(page.locator('[data-price]')).toContainText('CAD');
});

Screenshot comparison (visual regression):

test('Product page screenshot matches baseline', async ({ page }) => {
  await page.goto('/products/sample-product');
  await expect(page).toHaveScreenshot();
});

Best Practices for Shopify E2E Tests

Practice Why It Matters
Use data attributes for selection Selectors like button:has-text("Add to cart") break if copy changes. Use [data-add-to-cart] instead.
Wait for network requests to complete Add await page.waitForLoadState('networkidle') before assertions.
Test on production theme Don't test on a dev theme. Test on the actual theme your customers see.
Run tests on multiple browsers Test Chrome, Firefox, and Safari. CSS works differently on each.
Set up test data cleanup If tests create orders or accounts, clean them up afterward. Don't pollute your analytics.
Test edge cases Test invalid zip codes, expired credit cards, out-of-stock products.
Keep tests independent Each test should be able to run alone. Don't depend on previous test setup.

Performance: When to Use E2E vs. Unit Tests

E2E tests are expensive. They're slow (30+ seconds per test). Use them for critical user journeys:

  • Checkout flow
  • Product add-to-cart
  • Login (if you have accounts)
  • Search and filter
  • Payment processing

Use faster unit tests for:

  • Button click handlers (JavaScript logic)
  • Price calculations
  • Form validation
  • Discount code logic

Combination: 10% E2E tests, 90% unit tests.

Debugging Failed Tests

Playwright has excellent debugging tools:

# Debug mode (step through test)
npx playwright test --debug

# Headed mode with slowdown (see browser in slow-motion)
npx playwright test --headed --headed-slow-motion=1000

Browser DevTools also work in Playwright. You can inspect elements, console logs, and network activity during test runs.

Frequently Asked Questions

What's a good test coverage for a Shopify store?

Aim for 5–10 critical E2E tests covering: product page → cart → checkout → confirmation. These cover 80% of user journeys. Add 20–30 unit tests for edge cases and validation logic.

How long should E2E tests take?

1–3 minutes per test is normal. A full suite with 10 E2E tests runs in 15–30 minutes. If tests are slower, optimize selectors or consider reducing test count.

Can I test payment processing in E2E tests?

Yes, but use Stripe/Shopify's sandbox credentials. Never use real payment cards in tests. Sandbox credentials are: card number 4111111111111111, any expiry date, any CVV.

Can Playwright test Shopify Plus dynamic checkout?

Yes. Playwright can interact with Shopify's Checkout API. You may need to use iframe selectors to access payment inputs if they're in an iframe.

Should we test theme updates?

Yes. After every theme update (including Shopify's native updates), re-run all E2E tests. Theme changes can break checkout unexpectedly.

Author Perspective

We integrated Playwright into a client's CI/CD pipeline and caught 12 checkout bugs that would have shipped to production. One bug prevented specific shipping addresses from completing payment. Another broke for Firefox users. E2E tests caught both before customers did. The ROI on setting up tests is immediate—you ship faster with confidence.

Want to set up Playwright testing for your Shopify store? Tenten builds and maintains E2E test suites for Shopify merchants. Book a consultation to discuss your testing strategy.