Skip to content

End-to-End (E2E) Tests

Overview

E2E tests verify the complete module behavior in a real browser environment using Playwright. These tests ensure that CSS positioning, hydration, and browser-specific behaviors work correctly.

Location

E2E tests are in test/e2e/:

test/e2e/
├── basic.test.ts           # Module loading and basic functionality
├── positioning.test.ts     # CSS positioning in real browser
└── nuxt-config-flow.test.ts # Configuration from nuxt.config to DOM

Prerequisites

Install Playwright Browsers

bash
# Install Chromium (required for CI)
pnpm exec playwright-core install chromium

# Or install all browsers for local testing
pnpm exec playwright install

Running E2E Tests

bash
# Run all E2E tests
npx vitest run test/e2e

# Run with headed browser (see what's happening)
HEADED=true npx vitest run test/e2e

# Run specific test
npx vitest run test/e2e/positioning.test.ts

# Debug mode
PWDEBUG=1 npx vitest run test/e2e

What We Test

Module Loading (basic.test.ts)

Tests that the module loads correctly in Nuxt:

typescript
describe('Module loading', async () => {
  await setup({
    rootDir: fileURLToPath(new URL('../fixtures/basic', import.meta.url)),
  })

  it('renders the index page', async () => {
    const html = await $fetch('/')
    expect(html).toContain('<div>basic</div>')
  })

  it('loads SmartScript module', async () => {
    const html = await $fetch('/')
    expect(html).toContain('ss-sup')
  })
})

CSS Positioning (positioning.test.ts)

Tests actual browser rendering and positioning:

typescript
test('trademark positioning', async ({ page }) => {
  await page.goto('/test')
  
  // Wait for processing
  await page.waitForSelector('.ss-tm')
  
  // Check computed styles
  const tm = await page.locator('.ss-tm').first()
  const styles = await tm.evaluate(el => {
    const computed = window.getComputedStyle(el)
    return {
      position: computed.position,
      top: computed.top,
      fontSize: computed.fontSize
    }
  })
  
  expect(styles.position).toBe('relative')
  expect(styles.top).toBe('-0.4em')
})

Configuration Flow (nuxt-config-flow.test.ts)

Tests that configuration propagates correctly:

typescript
test('CSS variables from config', async ({ page }) => {
  // Start Nuxt with custom config
  const nuxt = await setup({
    nuxtConfig: {
      smartscript: {
        cssVariables: {
          'tm-top': '-0.6em'
        }
      }
    }
  })

  await page.goto('/')
  
  // Check CSS variable is applied
  const root = await page.locator(':root')
  const cssVar = await root.evaluate(() => 
    getComputedStyle(document.documentElement)
      .getPropertyValue('--ss-tm-top')
  )
  
  expect(cssVar).toBe('-0.6em')
})

Browser-Specific Testing

Hydration Testing

typescript
test('no hydration mismatch', async ({ page }) => {
  const errors: string[] = []
  
  // Listen for console errors
  page.on('console', msg => {
    if (msg.type() === 'error') {
      errors.push(msg.text())
    }
  })
  
  await page.goto('/')
  await page.waitForLoadState('networkidle')
  
  // Check for hydration errors
  const hydrationErrors = errors.filter(e => 
    e.includes('Hydration') || e.includes('mismatch')
  )
  
  expect(hydrationErrors).toHaveLength(0)
})

Visual Regression

typescript
test('visual appearance', async ({ page }) => {
  await page.goto('/showcase')
  await page.waitForSelector('.ss-sup')
  
  // Take screenshot for visual comparison
  await expect(page).toHaveScreenshot('showcase.png', {
    fullPage: true,
    animations: 'disabled'
  })
})

Performance Testing

typescript
test('processing performance', async ({ page }) => {
  await page.goto('/large-document')
  
  // Measure processing time
  const processingTime = await page.evaluate(() => {
    const start = performance.now()
    // Trigger processing
    window.$smartscript.processAll()
    return performance.now() - start
  })
  
  expect(processingTime).toBeLessThan(1000) // Under 1 second
})

Playwright Configuration

E2E tests use specific Playwright settings:

typescript
import { chromium, type Page } from 'playwright-core'

let browser: Browser
let page: Page

beforeAll(async () => {
  browser = await chromium.launch({
    headless: !process.env.HEADED
  })
})

beforeEach(async () => {
  page = await browser.newPage()
})

afterEach(async () => {
  await page.close()
})

afterAll(async () => {
  await browser.close()
})

Test Fixtures

E2E tests use fixtures in test/fixtures/:

test/fixtures/
└── basic/
    ├── nuxt.config.ts
    ├── app.vue
    └── pages/
        └── index.vue

Example fixture:

vue
<!-- test/fixtures/basic/app.vue -->
<template>
  <div>
    <h1>Test Page</h1>
    <p>H2O and CO2 at 25°C</p>
    <p>Product(TM) and Company(R)</p>
    <p>E=mc^2 where c is speed of light</p>
  </div>
</template>

Best Practices

1. Use Page Objects

Create reusable page objects:

typescript
class SmartScriptPage {
  constructor(private page: Page) {}
  
  async goto(path = '/') {
    await this.page.goto(path)
    await this.waitForProcessing()
  }
  
  async waitForProcessing() {
    await this.page.waitForSelector('.ss-sup, .ss-sub', {
      timeout: 5000
    })
  }
  
  async getTransformedElements() {
    return this.page.$$('.ss-sup, .ss-sub')
  }
}

2. Test User Interactions

typescript
test('interactive elements remain clickable', async ({ page }) => {
  await page.goto('/')
  
  // Add button with trademark
  await page.evaluate(() => {
    const btn = document.createElement('button')
    btn.textContent = 'Click Me(TM)'
    btn.onclick = () => alert('Clicked!')
    document.body.appendChild(btn)
  })
  
  // Wait for processing
  await page.waitForSelector('.ss-tm')
  
  // Button should still be clickable
  page.on('dialog', dialog => dialog.accept())
  await page.click('button')
})

3. Test Multiple Browsers

typescript
const browsers = ['chromium', 'firefox', 'webkit']

browsers.forEach(browserName => {
  test.describe(`${browserName}`, () => {
    // Browser-specific tests
  })
})

Debugging E2E Tests

Visual Debugging

bash
# Run with headed browser
HEADED=true npx vitest run test/e2e

# Use Playwright Inspector
PWDEBUG=1 npx vitest run test/e2e

# Slow down execution
SLOW_MO=1000 npx vitest run test/e2e

Console Logging

typescript
test('debug example', async ({ page }) => {
  // Log all console messages
  page.on('console', msg => console.log('PAGE:', msg.text()))
  
  // Log network requests
  page.on('request', req => console.log('REQUEST:', req.url()))
  
  await page.goto('/')
})

Screenshots on Failure

typescript
test('with screenshot', async ({ page }) => {
  try {
    await page.goto('/')
    // Test logic
  } catch (error) {
    await page.screenshot({ 
      path: `test-failure-${Date.now()}.png` 
    })
    throw error
  }
})

CI/CD Integration

Our GitHub Actions workflow includes:

yaml
- name: Install Playwright
  run: pnpm exec playwright-core install chromium

- name: Test E2E
  run: npx vitest run test/e2e

Performance Considerations

E2E tests are the slowest but most realistic:

  • Average runtime: 1-3 seconds per test
  • Run in CI on every push
  • Use parallelization when possible
  • Cache browser installations
  • Run critical paths only in CI, full suite nightly

Apache 2.0 License