Detailed comparison of testing frameworks for Chrome extension regression testing in CI/CD.
TL;DR: Playwright is the best choice for EchoKit. It’s already in use, has excellent Chrome extension support, and works in headless CI with the chromium channel.
| Feature | Playwright ⭐ | Puppeteer | Selenium | WebDriverIO |
|---|---|---|---|---|
| Chrome Extension Support | ✅ Excellent | ✅ Good | ⚠️ Fair | ✅ Good |
| Headless Mode | ✅ Yes (channel: 'chromium') |
✅ Yes (new headless) |
❌ Limited | ✅ Yes |
| Service Worker Support | ✅ First-class | ✅ Good | ⚠️ Doesn’t terminate | ✅ Good |
| Cross-Browser | ✅ Chrome, Firefox, Safari | ❌ Chrome only | ✅ All browsers | ✅ All browsers |
| CI/CD Support | ✅ Excellent | ✅ Good | ⚠️ Fair | ✅ Good |
| Documentation | ✅ Excellent | ✅ Good | ✅ Extensive | ✅ Good |
| Learning Curve | 🟢 Low | 🟢 Low | 🟡 Medium | 🟡 Medium |
| Speed | ⚡ Fast | ⚡ Very Fast | 🐌 Slower | ⚡ Fast |
| Screenshot/Video | ✅ Built-in | ✅ Built-in | ⚠️ Requires plugins | ✅ Built-in |
| Network Interception | ✅ Advanced | ✅ Advanced | ⚠️ Limited | ✅ Good |
| Community Support | 🔥 Very Active | 🔥 Active | 🔥 Very Active | 🟢 Active |
| Maintenance | ✅ Microsoft | ✅ Selenium Project | ✅ Community | |
| License | Apache 2.0 | Apache 2.0 | Apache 2.0 | MIT |
Best For: Modern web apps, Chrome extensions, cross-browser testing
✅ Native Chrome Extension API - First-class support for extensions
✅ Headless Extensions - Works in CI with channel: 'chromium'
✅ Service Worker Handling - Reliable extension loading
✅ Auto-wait - Smart waits reduce flakiness
✅ Network Mocking - Intercept and mock requests
✅ Multi-language - JavaScript, Python, Java, .NET
✅ Active Development - Microsoft-backed, frequent updates
❌ Persistent Context - Extensions require launchPersistentContext (more setup)
❌ Memory Usage - Slightly higher than Puppeteer
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
context = p.chromium.launch_persistent_context(
"",
channel='chromium', # Key for CI headless support
headless=True,
args=[
'--disable-extensions-except=/path/to/extension',
'--load-extension=/path/to/extension'
]
)
# Wait for service worker
sw = context.wait_for_event('serviceworker')
extension_id = sw.url.split('/')[2]
# Open popup
page = context.new_page()
page.goto(f'chrome-extension://{extension_id}/popup.html')
page.wait_for_selector('[data-testid="app"]')
# Test interaction
page.click('button[data-action="record"]')
assert page.locator('.status').text_content() == 'Recording'
- name: Install Playwright
run: |
pip install playwright
python -m playwright install chromium
- name: Run tests
run: python tests/smoke_test.py
Runtime: ~1-2 minutes for 20 tests
Best For: Chrome-only projects, performance testing
✅ Lightweight - Minimal dependencies
✅ Fast - Faster than Playwright for Chrome-only
✅ Google-maintained - Official Chrome DevTools Protocol wrapper
✅ New Headless Mode - headless: 'new' supports extensions
❌ Chrome Only - No Firefox/Safari support
❌ Extension API - Less mature than Playwright
❌ Python Support - Via pyppeteer (community-maintained)
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--disable-extensions-except=/path/to/extension',
'--load-extension=/path/to/extension'
]
});
// Wait for service worker
const workerTarget = await browser.waitForTarget(
target => target.type() === 'service_worker'
);
const worker = await workerTarget.worker();
const extensionId = workerTarget.url().split('/')[2];
const page = await browser.newPage();
await page.goto(`chrome-extension://${extensionId}/popup.html`);
await page.waitForSelector('[data-testid="app"]');
await page.click('button[data-action="record"]');
await browser.close();
})();
Runtime: ~45-90 seconds for 20 tests
Best For: Legacy systems, multi-browser compliance testing
✅ Mature - 15+ years of development
✅ Wide Browser Support - Chrome, Firefox, Safari, IE, Edge
✅ Large Ecosystem - Tons of plugins and integrations
✅ Multiple Languages - Java, Python, C#, Ruby, JavaScript
❌ Service Workers - ChromeDriver keeps debugger attached (workers don’t terminate)
❌ Slower - More overhead than modern frameworks
❌ Extension Support - Less elegant than Playwright/Puppeteer
❌ Headless Limitations - Extension loading is tricky
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument('--load-extension=/path/to/extension')
options.add_argument('--headless=new')
driver = webdriver.Chrome(options=options)
# Get extension ID (requires ChromeDriver workaround)
driver.get('chrome://extensions/')
# ... complex ID extraction ...
driver.get(f'chrome-extension://{extension_id}/popup.html')
element = driver.find_element(By.CSS_SELECTOR, '[data-testid="app"]')
driver.quit()
Runtime: ~2-4 minutes for 20 tests
Best For: Complex test suites with advanced reporting needs
✅ Feature-Rich - Built-in test runner, reporters, services
✅ Chrome Extension Support - Good extension APIs
✅ Developer Experience - Nice CLI and debugging tools
❌ JavaScript Only - No Python/Java
❌ Complexity - More setup than Playwright
❌ Overhead - Slower than Playwright for simple tests
Runtime: ~2-3 minutes for 20 tests
| Framework | GitHub Actions | GitLab CI | Circle CI | Jenkins |
|---|---|---|---|---|
| Playwright | ✅ Excellent | ✅ Excellent | ✅ Good | ✅ Good |
| Puppeteer | ✅ Excellent | ✅ Excellent | ✅ Good | ✅ Good |
| Selenium | ⚠️ Fair | ⚠️ Fair | ⚠️ Fair | ✅ Good |
| WebDriverIO | ✅ Good | ✅ Good | ✅ Good | ✅ Good |
| Framework | Percy | Chromatic | Applitools | Built-in |
|---|---|---|---|---|
| Playwright | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Screenshots |
| Puppeteer | ✅ Yes | ⚠️ Limited | ✅ Yes | ✅ Screenshots |
| Selenium | ✅ Yes | ❌ No | ✅ Yes | ⚠️ Via plugins |
| WebDriverIO | ✅ Yes | ⚠️ Limited | ✅ Yes | ✅ Screenshots |
For the 70% unit test layer of the test pyramid:
| Framework | Best For | Speed | Chrome APIs |
|---|---|---|---|
| Vitest ⭐ | Modern ESM projects | ⚡⚡⚡ | ✅ Mock via vi.mock() |
| Jest | React/Node projects | ⚡⚡ | ✅ Mock via jest.mock() |
| Mocha + Chai | Flexibility | ⚡⚡ | ⚠️ Manual mocking |
Recommendation for EchoKit: Vitest
┌─────────────────────────────────────────┐
│ Unit Tests (70%) │
│ Tool: Vitest │
│ Runtime: < 10 seconds │
│ Files: tests/unit/*.test.js │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Integration Tests (20%) │
│ Tool: Vitest + Chrome API mocks │
│ Runtime: 10-20 seconds │
│ Files: tests/integration/*.test.js │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ E2E Tests (10%) │
│ Tool: Playwright ⭐ │
│ Runtime: 1-2 minutes │
│ Files: tests/smoke_echokit.py │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ Visual Regression (Optional) │
│ Tool: Playwright Screenshots │
│ Fallback: Percy.io (if budget allows) │
│ Runtime: +30 seconds │
└─────────────────────────────────────────┘
tests/smoke_echokit.py existschannel='chromium'No changes needed! Just:
smoke_echokit.py to use channel='chromium'if: false from .github/workflows/test.ymlSymptom: Tests fail randomly with “Extension not found”
Root Cause: Race condition waiting for service worker
Solution (Playwright):
# ❌ Bad - assumes service worker exists
sw = context.service_workers[0]
# ✅ Good - waits for service worker
sw = context.wait_for_event('serviceworker', timeout=30000)
Solution (Puppeteer):
// ✅ Good - explicit wait
const workerTarget = await browser.waitForTarget(
target => target.type() === 'service_worker',
{ timeout: 30000 }
);
Symptom: Hard-coded extension ID fails in CI
Root Cause: Extension ID is deterministic only with --load-extension
Solution:
# ❌ Bad - hard-coded
popup_url = 'chrome-extension://abcdefghijklmnop/popup.html'
# ✅ Good - dynamic extraction
sw = context.wait_for_event('serviceworker')
extension_id = sw.url.split('/')[2]
popup_url = f'chrome-extension://{extension_id}/popup.html'
Symptom: Tests pass locally (headed) but fail in CI (headless)
Common Causes:
Solutions:
# Set consistent viewport
context = p.chromium.launch_persistent_context(
"",
viewport={'width': 1280, 'height': 720}, # Fixed size
device_scale_factor=1, # No retina
...
)
# Install fonts in CI (if needed)
# .github/workflows/test.yml:
- run: sudo apt-get install -y fonts-liberation
Symptom: Extension state leaks between tests in Selenium
Root Cause: ChromeDriver attaches debugger, preventing worker termination
Solution: Use Playwright or Puppeteer instead, OR:
# Restart browser between tests (slow but reliable)
def tearDown(self):
self.driver.quit()
def setUp(self):
self.driver = webdriver.Chrome(options=self.options)
Test suite: 20 test cases (EchoKit smoke tests)
| Framework | Headed | Headless | CI (GitHub Actions) |
|---|---|---|---|
| Playwright | 60s | 45s | 75s (with setup) |
| Puppeteer | 55s | 40s | 70s (with setup) |
| Selenium | 120s | N/A | 180s (with setup) |
Benchmarked on: GitHub Actions ubuntu-latest, 2-core runner
Recommendation: Start with free Playwright screenshots. Evaluate paid services after 3 months if baseline management becomes burdensome.
Use this to choose based on your constraints:
| If you need… | Choose… |
|---|---|
| Chrome extension testing in CI | Playwright |
| Fastest test execution | Puppeteer |
| Multi-browser support | Playwright or Selenium |
| Python test suite | Playwright |
| JavaScript test suite | Puppeteer or Playwright |
| Legacy browser support (IE11) | Selenium |
| Visual regression (free) | Playwright screenshots |
| Visual regression (managed) | Percy.io |
| Zero learning curve (already using…) | Keep existing |
For EchoKit specifically: Playwright (already in use, works in CI, Python-friendly)
Recommendation for EchoKit:
channel='chromium' to enable headlessEstimated effort: 4-8 hours to implement Phase 1 (CI fixes) Expected ROI: 10x reduction in production bugs caught early
Last Updated: 2026-05-12
See Also: CI_CD_IMPLEMENTATION_GUIDE.md, CHROME_EXTENSION_CI_CD_TESTING_RESEARCH.md