hako/e2e/e2eutil/helper.go
Felipe M. d71915a3fb
test(e2e): expand test suite with comprehensive coverage
- Add e2e tests for admin pages (archivers, extractors, rules)
- Add tests for archives, links, navigation, and error handling
- Add search and pagination test coverage
- Implement test web server with Docker container
- Add seed data and helper utilities for tests
- Update webapp components for permission handling
- Generate HTML reports with embedded screenshots

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-29 12:49:40 +01:00

455 lines
12 KiB
Go

package e2eutil
import (
"fmt"
"path/filepath"
"strings"
"testing"
"time"
"github.com/playwright-community/playwright-go"
"github.com/stretchr/testify/require"
)
var globalReporter *Reporter
// TestHelper provides utility methods for E2E testing.
type TestHelper struct {
t *testing.T
testName string
baseURL string
page playwright.Page
screenshotNum int
screenshotDir string
}
// NewTestHelper creates a new TestHelper instance.
func NewTestHelper(t *testing.T, testName string, baseURL string, page playwright.Page) *TestHelper {
t.Helper()
// Initialize reporter if not already set
if globalReporter == nil {
globalReporter = GetReporter()
}
// Get screenshot directory
screenshotDir := filepath.Join("..", "screenshots")
h := &TestHelper{
t: t,
testName: testName,
baseURL: baseURL,
page: page,
screenshotNum: 1,
screenshotDir: screenshotDir,
}
// Initialize test in reporter
h.recordResult("Test started", true, nil, "", 0)
// Automatically mark test as complete when it finishes
t.Cleanup(func() {
if globalReporter != nil {
globalReporter.TestFinished(t)
}
})
return h
}
// recordResult is a helper to record results to the reporter if available
func (h *TestHelper) recordResult(description string, passed bool, err error, screenshotPath string, duration time.Duration) {
if globalReporter != nil {
globalReporter.AddResult(h.testName, description, passed, err, screenshotPath, duration)
}
}
// Navigate navigates to the given path relative to the base URL.
func (h *TestHelper) Navigate(path string) error {
h.t.Helper()
url := h.baseURL + path
h.t.Logf("Navigating to %s", url)
_, err := h.page.Goto(url)
return err
}
// Fill fills an input field with the given value.
func (h *TestHelper) Fill(selector, value string) error {
h.t.Helper()
h.t.Logf("Filling %s with value", selector)
return h.page.Fill(selector, value)
}
// Click clicks on an element matching the given selector.
func (h *TestHelper) Click(selector string) error {
h.t.Helper()
h.t.Logf("Clicking %s", selector)
return h.page.Click(selector)
}
// Screenshot captures a screenshot with an auto-incrementing number.
// Returns the path to the saved screenshot.
func (h *TestHelper) Screenshot(name string) string {
h.t.Helper()
// Create filename with number prefix
filename := fmt.Sprintf("%02d-%s.png", h.screenshotNum, name)
h.screenshotNum++
// Full path
fullPath := filepath.Join(h.screenshotDir, filename)
// Capture screenshot
_, err := h.page.Screenshot(playwright.PageScreenshotOptions{
Path: playwright.String(fullPath),
})
if err != nil {
h.t.Logf("Failed to capture screenshot: %v", err)
return ""
}
h.t.Logf("Screenshot saved: %s", fullPath)
return fullPath
}
// WaitForSelector waits for an element matching the selector to appear.
func (h *TestHelper) WaitForSelector(selector string, timeout time.Duration) error {
h.t.Helper()
h.t.Logf("Waiting for selector %s", selector)
_, err := h.page.WaitForSelector(selector, playwright.PageWaitForSelectorOptions{
Timeout: playwright.Float(float64(timeout.Milliseconds())),
})
return err
}
// AssertTextContains asserts that an element contains the expected text.
func (h *TestHelper) AssertTextContains(selector, expected string) {
h.t.Helper()
start := time.Now()
text, err := h.page.Locator(selector).TextContent()
if err != nil {
duration := time.Since(start)
screenshot := h.Screenshot("assertion-failure-text-content")
h.recordResult(fmt.Sprintf("Assert text in %s contains %q", selector, expected), false,
fmt.Errorf("Failed to get text content of %s: %v", selector, err), screenshot, duration)
require.NoError(h.t, err, "Failed to get text content of %s", selector)
return
}
passed := strings.Contains(text, expected)
var screenshot string
var resultErr error
if !passed {
resultErr = fmt.Errorf("Element %s should contain text %q, got %q", selector, expected, text)
screenshot = h.Screenshot("assertion-failure-text-contains")
}
duration := time.Since(start)
h.recordResult(fmt.Sprintf("Assert text in %s contains %q", selector, expected), passed, resultErr, screenshot, duration)
require.Contains(h.t, text, expected, "Element %s should contain text %q", selector, expected)
}
// AssertURL asserts that the current URL contains the expected string.
func (h *TestHelper) AssertURL(expected string) {
h.t.Helper()
start := time.Now()
url := h.page.URL()
passed := strings.Contains(url, expected)
var err error
var screenshot string
if !passed {
err = fmt.Errorf("URL should contain %q, got %q", expected, url)
screenshot = h.Screenshot("assertion-failure-url")
}
duration := time.Since(start)
h.recordResult(fmt.Sprintf("Assert URL contains %q", expected), passed, err, screenshot, duration)
require.Contains(h.t, url, expected, "URL should contain %q, got %q", expected, url)
h.t.Logf("URL contains %q: %s", expected, url)
}
// GetURL returns the current page URL.
func (h *TestHelper) GetURL() string {
return h.page.URL()
}
// IsVisible checks if an element matching the selector is visible.
func (h *TestHelper) IsVisible(selector string) bool {
h.t.Helper()
visible, err := h.page.Locator(selector).IsVisible()
if err != nil {
h.t.Logf("Error checking visibility of %s: %v", selector, err)
return false
}
return visible
}
// GetText returns the text content of an element.
func (h *TestHelper) GetText(selector string) (string, error) {
h.t.Helper()
return h.page.Locator(selector).TextContent()
}
// AssertNotVisible asserts that an element is not visible.
func (h *TestHelper) AssertNotVisible(selector string, message string) {
h.t.Helper()
start := time.Now()
visible := h.IsVisible(selector)
passed := !visible
if message == "" {
message = fmt.Sprintf("Element %s should not be visible", selector)
}
var err error
var screenshot string
if !passed {
err = fmt.Errorf("%s", message)
screenshot = h.Screenshot("assertion-failure-not-visible")
}
duration := time.Since(start)
h.recordResult(fmt.Sprintf("Assert %s not visible", selector), passed, err, screenshot, duration)
require.False(h.t, visible, message)
}
// AssertVisible asserts that an element is visible.
func (h *TestHelper) AssertVisible(selector string, message string) {
h.t.Helper()
start := time.Now()
visible := h.IsVisible(selector)
passed := visible
if message == "" {
message = fmt.Sprintf("Element %s should be visible", selector)
}
var err error
var screenshot string
if !passed {
err = fmt.Errorf("%s", message)
screenshot = h.Screenshot("assertion-failure-visible")
}
duration := time.Since(start)
h.recordResult(fmt.Sprintf("Assert %s visible", selector), passed, err, screenshot, duration)
require.True(h.t, visible, message)
}
// GetCurrentPath returns the path portion of the current URL (without baseURL).
func (h *TestHelper) GetCurrentPath() string {
h.t.Helper()
url := h.page.URL()
// Remove baseURL to get just the path
path := strings.TrimPrefix(url, h.baseURL)
return path
}
// LoginAsAdmin logs in as admin user with default credentials.
func (h *TestHelper) LoginAsAdmin() error {
h.t.Helper()
// Navigate to login page
if err := h.Navigate("/login"); err != nil {
return fmt.Errorf("failed to navigate to login page: %w", err)
}
// Wait for login form
if err := h.WaitForSelector("#email", 5*time.Second); err != nil {
return fmt.Errorf("email input not found: %w", err)
}
// Fill credentials
if err := h.Fill("#email", "hako@hako.com"); err != nil {
return fmt.Errorf("failed to fill email: %w", err)
}
if err := h.Fill("#password", "hako"); err != nil {
return fmt.Errorf("failed to fill password: %w", err)
}
// Submit form
if err := h.Click("button[type=\"submit\"]"); err != nil {
return fmt.Errorf("failed to click submit: %w", err)
}
// Wait for redirect to home
time.Sleep(2 * time.Second)
h.t.Log("Logged in as admin")
return nil
}
// Logout logs out the current user.
func (h *TestHelper) Logout() error {
h.t.Helper()
// Click logout button (adjust selector based on actual UI)
if err := h.Click("a[href=\"/logout\"]"); err != nil {
return fmt.Errorf("failed to click logout: %w", err)
}
time.Sleep(1 * time.Second)
h.t.Log("Logged out")
return nil
}
// CreateLink creates a new link via the Quick Add form.
func (h *TestHelper) CreateLink(url string) error {
h.t.Helper()
// Wait for URL input on home page
if err := h.WaitForSelector("input[name=\"url\"]", 5*time.Second); err != nil {
return fmt.Errorf("URL input not found: %w", err)
}
// Fill URL
if err := h.Fill("input[name=\"url\"]", url); err != nil {
return fmt.Errorf("failed to fill URL: %w", err)
}
// Submit form
if err := h.Click("button[type=\"submit\"]"); err != nil {
return fmt.Errorf("failed to submit form: %w", err)
}
// Wait for redirect to /links page
start := time.Now()
for time.Since(start) < 5*time.Second {
currentURL := h.GetURL()
if strings.Contains(currentURL, "/links") {
h.t.Logf("Successfully redirected to links page")
break
}
time.Sleep(500 * time.Millisecond)
}
h.t.Logf("Created link: %s", url)
return nil
}
// SearchLinks performs a search for links.
func (h *TestHelper) SearchLinks(query string) error {
h.t.Helper()
// Wait for search input
if err := h.WaitForSelector("input[name=\"search\"]", 5*time.Second); err != nil {
return fmt.Errorf("search input not found: %w", err)
}
// Fill search query
if err := h.Fill("input[name=\"search\"]", query); err != nil {
return fmt.Errorf("failed to fill search query: %w", err)
}
// Submit search (might be automatic or require button click)
time.Sleep(1 * time.Second)
h.t.Logf("Searched for: %s", query)
return nil
}
// WaitForText waits for an element to contain specific text.
func (h *TestHelper) WaitForText(selector, text string, timeout time.Duration) error {
h.t.Helper()
start := time.Now()
for time.Since(start) < timeout {
content, err := h.GetText(selector)
if err == nil && strings.Contains(content, text) {
return nil
}
time.Sleep(500 * time.Millisecond)
}
return fmt.Errorf("timeout waiting for text %q in selector %s", text, selector)
}
// WaitForElementCount waits for a specific number of elements matching the selector.
func (h *TestHelper) WaitForElementCount(selector string, count int, timeout time.Duration) error {
h.t.Helper()
start := time.Now()
for time.Since(start) < timeout {
elements, err := h.page.Locator(selector).All()
if err == nil && len(elements) == count {
return nil
}
time.Sleep(500 * time.Millisecond)
}
return fmt.Errorf("timeout waiting for %d elements matching %s", count, selector)
}
// SelectDropdown selects an option from a dropdown.
func (h *TestHelper) SelectDropdown(selector, value string) error {
h.t.Helper()
h.t.Logf("Selecting %s from dropdown %s", value, selector)
_, err := h.page.SelectOption(selector, playwright.SelectOptionValues{Values: &[]string{value}})
return err
}
// WaitForArchiveComplete waits for an archive to complete processing.
func (h *TestHelper) WaitForArchiveComplete(timeout time.Duration) error {
h.t.Helper()
// Wait for status indicator to show "completed" or similar
// This is a generic implementation - adjust based on actual UI
return h.WaitForText(".archive-status", "completed", timeout)
}
// GetArchiveStatus returns the current archive status.
func (h *TestHelper) GetArchiveStatus() (string, error) {
h.t.Helper()
return h.GetText(".archive-status")
}
// FillForm fills multiple form fields at once.
func (h *TestHelper) FillForm(fields map[string]string) error {
h.t.Helper()
for selector, value := range fields {
if err := h.Fill(selector, value); err != nil {
return fmt.Errorf("failed to fill %s: %w", selector, err)
}
}
return nil
}
// GetElementCount returns the number of elements matching the selector.
func (h *TestHelper) GetElementCount(selector string) (int, error) {
h.t.Helper()
elements, err := h.page.Locator(selector).All()
if err != nil {
return 0, err
}
return len(elements), nil
}
// WaitForPath waits for the current path to contain the expected path.
func (h *TestHelper) WaitForPath(expectedPath string, timeout time.Duration) error {
h.t.Helper()
start := time.Now()
for time.Since(start) < timeout {
currentPath := h.GetCurrentPath()
if strings.Contains(currentPath, expectedPath) {
return nil
}
time.Sleep(100 * time.Millisecond)
}
return fmt.Errorf("timeout waiting for path containing %q", expectedPath)
}