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) }