test: add mock-based site extractor test infrastructure
All checks were successful
CI / vet (pull_request) Successful in 1m5s
CI / build (pull_request) Successful in 1m6s
CI / test (pull_request) Successful in 1m6s

Create exported extractortest package with MockBrowser, MockDocument,
and MockNode that support selector-based responses for testing site
extractors without a real browser.

Add extraction tests for DuckDuckGo (result parsing, empty results, no
links, full search flow) and Powerball (drawing parsing, next drawing
parsing with billion/million, error cases, full GetCurrent flow).

Closes #21
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 16:37:58 +00:00
parent ddb701fca0
commit 198906946b
3 changed files with 407 additions and 0 deletions

103
extractortest/mock.go Normal file
View File

@@ -0,0 +1,103 @@
// Package extractortest provides mock implementations of the extractor
// interfaces for use in unit tests. The mocks support selector-based
// responses so site extractors can exercise their parsing logic without
// a real browser.
package extractortest
import (
"context"
"fmt"
"time"
"gitea.stevedudenhoeffer.com/steve/go-extractor"
)
// MockNode implements extractor.Node with configurable selector responses.
type MockNode struct {
TextValue string
TextErr error
ContentValue string
Attrs map[string]string
// Children maps CSS selectors to the nodes that should be returned.
Children map[string]extractor.Nodes
}
var _ extractor.Node = &MockNode{}
func (m *MockNode) Content() (string, error) { return m.ContentValue, nil }
func (m *MockNode) Text() (string, error) { return m.TextValue, m.TextErr }
func (m *MockNode) Attr(name string) (string, error) {
if m.Attrs == nil {
return "", nil
}
v, ok := m.Attrs[name]
if !ok {
return "", nil
}
return v, nil
}
func (m *MockNode) Screenshot() ([]byte, error) { return nil, nil }
func (m *MockNode) Type(_ string) error { return nil }
func (m *MockNode) Click() error { return nil }
func (m *MockNode) SetHidden(_ bool) error { return nil }
func (m *MockNode) SetAttribute(_, _ string) error { return nil }
func (m *MockNode) Select(selector string) extractor.Nodes {
if m.Children == nil {
return nil
}
return m.Children[selector]
}
func (m *MockNode) SelectFirst(selector string) extractor.Node {
return m.Select(selector).First()
}
func (m *MockNode) ForEach(selector string, fn func(extractor.Node) error) error {
nodes := m.Select(selector)
for _, n := range nodes {
if err := fn(n); err != nil {
return err
}
}
return nil
}
// MockDocument implements extractor.Document with configurable selector responses.
type MockDocument struct {
MockNode
URLValue string
}
var _ extractor.Document = &MockDocument{}
func (m *MockDocument) URL() string { return m.URLValue }
func (m *MockDocument) Refresh() error { return nil }
func (m *MockDocument) Close() error { return nil }
func (m *MockDocument) WaitForNetworkIdle(_ *time.Duration) error { return nil }
// Content on MockDocument returns its own ContentValue (overrides embedded MockNode).
func (m *MockDocument) Content() (string, error) { return m.ContentValue, nil }
// MockBrowser implements extractor.Browser. Configure Documents to map URLs
// to MockDocuments that will be returned from Open.
type MockBrowser struct {
Documents map[string]*MockDocument
}
var _ extractor.Browser = &MockBrowser{}
func (m *MockBrowser) Close() error { return nil }
func (m *MockBrowser) Open(_ context.Context, url string, _ extractor.OpenPageOptions) (extractor.Document, error) {
if m.Documents == nil {
return nil, fmt.Errorf("no documents configured")
}
doc, ok := m.Documents[url]
if !ok {
return nil, fmt.Errorf("no mock document for URL %q", url)
}
return doc, nil
}