README
¶
go-playwright-testkit
Battle-tested Playwright wrapper for Go with graceful degradation and observability.
Extracted from a production Hugo site with 45+ test functions, this library provides clean abstractions over playwright-go with patterns proven in real-world browser testing.
Features
- 🎠Clean Playwright wrapper - Simple API over playwright-go bindings
- 🔄 Graceful degradation - Tests skip (not fail) when Playwright unavailable
- ✅ Battle-tested patterns - Error handling and timeouts tuned from production
- 🧪 TestContext abstraction - Unified test setup with pluggable server support
- 📊 OpenTelemetry integration - Optional observability (Logfire, Jaeger, custom)
- ♿ Accessibility scanning - Optional axe-core integration with pluggable reporting
- 🚀 Production-ready - Proven in real-world browser testing scenarios
Installation
go get github.com/pwarnock/go-playwright-testkit
Prerequisites
Install Playwright browsers:
go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps chromium
Quick Start
package main_test
import (
"testing"
"github.com/pwarnock/go-playwright-testkit/pkg/browser"
)
func TestBasicNavigation(t *testing.T) {
browser.SkipIfUnavailable(t)
b, err := browser.NewBrowser()
if err != nil {
t.Fatal(err)
}
defer b.Close()
err = b.NavigateTo("https://example.com")
if err != nil {
t.Fatal(err)
}
url := b.GetURL()
if url != "https://example.com/" {
t.Errorf("Expected https://example.com/, got %s", url)
}
}
Core Concepts
Browser Wrapper
The browser package provides a clean interface to Playwright:
// Create browser with default options (headless=true)
b, err := browser.NewBrowser()
// Or with custom options
opts := browser.BrowserOptions{
Headless: false, // Show browser window
}
b, err := browser.NewBrowserWithOptions(opts)
// Navigate and interact
b.NavigateTo("https://example.com")
b.ClickElement("#button")
b.WaitForSelector(".result")
visible, _ := b.IsElementVisible(".message")
b.TakeScreenshot("/tmp/screenshot.png")
Environment Variables:
PLAYWRIGHT_HEADLESS=false- Override default headless mode
Graceful Degradation
Tests automatically skip when Playwright is not available:
func TestMyFeature(t *testing.T) {
browser.SkipIfUnavailable(t)
// Test only runs if Playwright is installed
}
This provides a better CI experience than hard failures, especially in environments where Playwright may not be available.
TestContext
The context package provides unified test setup with pluggable server support:
import (
"github.com/pwarnock/go-playwright-testkit/pkg/context"
"github.com/pwarnock/go-playwright-testkit/pkg/browser"
)
// Define your server (implements context.ServerManager)
type MyServer struct {
server *httptest.Server
}
func (s *MyServer) Start() error {
s.server = httptest.NewServer(...)
return nil
}
func (s *MyServer) Stop() error {
s.server.Close()
return nil
}
func (s *MyServer) GetBaseURL() string {
return s.server.URL
}
func (s *MyServer) IsReady() bool {
return s.server != nil
}
// Use in tests
func TestWithServer(t *testing.T) {
browser.SkipIfUnavailable(t)
tc := context.NewTestContext(t, "")
tc.Server = &MyServer{}
tc.Setup()
defer tc.Teardown()
tc.SetupBrowser()
tc.Browser.NavigateTo(tc.BaseURL)
// Use assertion helpers
tc.AssertNoError(err)
tc.AssertEqual(expected, actual)
tc.AssertTrue(condition)
}
Observability (Optional)
The logger and telemetry packages provide optional OpenTelemetry integration:
import (
"context"
"github.com/pwarnock/go-playwright-testkit/pkg/logger"
"github.com/pwarnock/go-playwright-testkit/pkg/telemetry"
)
func TestWithTelemetry(t *testing.T) {
ctx := context.Background()
// Initialize OTEL if enabled (Logfire, Jaeger, or custom endpoint)
if telemetry.IsOTELEnabled() {
shutdown, _ := telemetry.InitOTEL(ctx)
defer shutdown(ctx)
}
// Create structured logger
logger := logger.NewStructuredLogger("MyTest")
defer logger.Close()
logger.Logf("Starting test")
logger.LogPerformance("page_load", 123.45, "ms")
logger.LogError(err, "Something failed")
}
Environment Variables:
LOGFIRE_TOKEN- Enable Logfire backendOTEL_EXPORTER_OTLP_ENDPOINT- Custom OTEL endpoint- Or run Jaeger locally on
localhost:4317
If none are configured, logs print to console (graceful degradation).
Accessibility Scanning (Optional)
The scanner package integrates with axe-core for accessibility testing:
import "github.com/pwarnock/go-playwright-testkit/pkg/scanner"
func TestAccessibility(t *testing.T) {
if !scanner.IsAxeCoreAvailable() {
t.Skip("axe-core not available")
}
s := scanner.NewAccessibilityScanner("http://localhost:8080")
err := s.ScanPage("http://localhost:8080/")
if err != nil {
t.Fatal(err)
}
// Get issues by severity
critical := s.GetCriticalIssues()
serious := s.GetSeriousIssues()
// Summary
summary := s.GetSummary()
t.Logf("Found %d issues: %d critical, %d serious",
summary["total"], summary["critical"], summary["serious"])
}
Pluggable Issue Reporting:
Implement the IssueReporter interface to integrate with your issue tracker:
type MyIssueReporter struct {
// Your issue tracker client
}
func (r *MyIssueReporter) ReportIssue(issue scanner.AccessibilityIssue) error {
// Create issue in GitHub, Jira, etc.
return nil
}
// Use it
s := scanner.NewAccessibilityScanner(baseURL)
s.SetReporter(&MyIssueReporter{})
s.ReportCriticalAndSerious() // Auto-create issues
Examples
See the examples/ directory for comprehensive usage:
basic/- Simple browser automationwith-server/- Custom HTTP server integrationwith-telemetry/- OpenTelemetry observability
API Reference
Browser Package
type Browser
func NewBrowser() (*Browser, error)
func NewBrowserWithOptions(opts BrowserOptions) (*Browser, error)
func (b *Browser) Close() error
func (b *Browser) NavigateTo(url string) error
func (b *Browser) GetURL() string
func (b *Browser) ClickElement(selector string) error
func (b *Browser) WaitForSelector(selector string) error
func (b *Browser) IsElementVisible(selector string) (bool, error)
func (b *Browser) TakeScreenshot(path string) error
func (b *Browser) WaitForPageLoad() error
func (b *Browser) GetPage() playwright.Page
func (b *Browser) SetViewport(width, height int64) error
func (b *Browser) Evaluate(jsScript string) (interface{}, error)
func (b *Browser) WaitForFunction(jsFunction string) error
func IsPlaywrightAvailable() bool
func SkipIfUnavailable(t *testing.T)
Context Package
type ServerManager interface {
Start() error
Stop() error
GetBaseURL() string
IsReady() bool
}
type TestContext
func NewTestContext(t *testing.T, baseURL string) *TestContext
func (tc *TestContext) Setup() error
func (tc *TestContext) Teardown()
func (tc *TestContext) SetupBrowser() error
func (tc *TestContext) AssertNoError(err error, msgAndArgs ...interface{})
func (tc *TestContext) AssertEqual(expected, actual interface{}, msgAndArgs ...interface{})
func (tc *TestContext) AssertContains(s, contains interface{}, msgAndArgs ...interface{})
func (tc *TestContext) AssertTrue(value bool, msgAndArgs ...interface{})
func (tc *TestContext) TakeScreenshot(filename string) error
func (tc *TestContext) TakeScreenshotOnError(testName string)
func (tc *TestContext) WaitForServer(maxAttempts int) error
Logger Package
type StructuredLogger
func NewStructuredLogger(testName string) *StructuredLogger
func (sl *StructuredLogger) Logf(format string, args ...interface{})
func (sl *StructuredLogger) LogError(err error, msg string)
func (sl *StructuredLogger) LogPerformance(metricName string, value float64, unit string)
func (sl *StructuredLogger) LogAccessibility(violations []interface{})
func (sl *StructuredLogger) Close() error
func (sl *StructuredLogger) IsOTELEnabled() bool
Telemetry Package
func InitOTEL(ctx context.Context) (func(context.Context) error, error)
func IsOTELEnabled() bool
Scanner Package
type AccessibilityIssue struct {
ID string
Title string
Description string
Impact string
Tags []string
Selector string
URL string
Metadata map[string]interface{}
}
type IssueReporter interface {
ReportIssue(issue AccessibilityIssue) error
}
type AccessibilityScanner
func NewAccessibilityScanner(baseURL string) *AccessibilityScanner
func (as *AccessibilityScanner) SetReporter(reporter IssueReporter)
func (as *AccessibilityScanner) ScanPage(url string) error
func (as *AccessibilityScanner) GetIssues() []AccessibilityIssue
func (as *AccessibilityScanner) GetCriticalIssues() []AccessibilityIssue
func (as *AccessibilityScanner) GetSeriousIssues() []AccessibilityIssue
func (as *AccessibilityScanner) ReportAll() error
func (as *AccessibilityScanner) ReportCriticalAndSerious() error
func (as *AccessibilityScanner) GetSummary() map[string]int
func IsAxeCoreAvailable() bool
CI Integration
GitHub Actions
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Install Playwright
run: |
go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps chromium
- name: Run tests
run: go test -v ./...
Local Development
# Install Playwright browsers
go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps chromium
# Run tests
go test -v ./...
# Run with coverage
go test -v -race -coverprofile=coverage.out ./...
# View coverage
go tool cover -html=coverage.out
Design Patterns
Battle-Tested Timeouts
Timeouts are tuned from production experience:
- Navigation: 30 seconds (handles slow servers)
- Element interactions: 5 seconds (balance between responsiveness and reliability)
- Page load: Uses
networkidlestate for complete loading
Graceful Cleanup
The Browser.Close() method handles cleanup gracefully:
- Closes page, context, and browser in correct order
- Continues cleanup even if one step fails
- Logs errors without panicking
Optional Features
Core features (browser wrapper, test context) have no external dependencies beyond playwright-go.
Optional features (OTEL, accessibility) gracefully degrade when unavailable:
- Logger works without OTEL (prints to console)
- Scanner checks for axe-core before scanning
- Tests skip cleanly when tools unavailable
Contributing
Contributions welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please ensure:
- Tests pass (
go test ./...) - Code is formatted (
go fmt ./...) - Lint is clean (
golangci-lint run)
License
MIT License - see LICENSE for details.
Acknowledgments
- Built on playwright-go
- Inspired by battle-tested patterns from production Hugo site testing
- OpenTelemetry integration for modern observability
Related Projects
- playwright-go - Official Playwright bindings for Go
- playwright - Cross-browser automation library
- axe-core - Accessibility testing engine