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:
- Visits a product page
- Clicks "Add to cart"
- Navigates to the cart
- Verifies the product is in the cart
- Proceeds to checkout
- 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.