- 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>
455 lines
12 KiB
Go
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)
|
|
}
|