fix(prefs): persist favicon choice and apply to HTML results
- Save favicon cookie on POST /preferences; reflect selection in template - Add getFaviconService helper; pass favicon service into FromResponse - Compute ResultView.FaviconIconURL (none/google/duckduckgo/self proxy) - Update result_item and video_item templates; add httpapi/views tests Made-with: Cursor
This commit is contained in:
parent
518215f62e
commit
90ea4c9f56
7 changed files with 171 additions and 43 deletions
|
|
@ -45,7 +45,7 @@ func NewHandler(searchSvc *search.Service, autocompleteSuggestions func(ctx cont
|
||||||
searchSvc: searchSvc,
|
searchSvc: searchSvc,
|
||||||
autocompleteSvc: autocompleteSuggestions,
|
autocompleteSvc: autocompleteSuggestions,
|
||||||
sourceURL: sourceURL,
|
sourceURL: sourceURL,
|
||||||
faviconCache: faviconCache,
|
faviconCache: faviconCache,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,6 +65,17 @@ func (h *Handler) getTheme(r *http.Request) string {
|
||||||
return "light"
|
return "light"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getFaviconService returns the favicon provider from cookie (default "none").
|
||||||
|
func (h *Handler) getFaviconService(r *http.Request) string {
|
||||||
|
if cookie, err := r.Cookie("favicon"); err == nil {
|
||||||
|
switch cookie.Value {
|
||||||
|
case "none", "google", "duckduckgo", "self":
|
||||||
|
return cookie.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
|
||||||
// Index renders the homepage with the search box.
|
// Index renders the homepage with the search box.
|
||||||
func (h *Handler) Index(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) Index(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/" {
|
if r.URL.Path != "/" {
|
||||||
|
|
@ -103,7 +114,7 @@ func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
|
||||||
req, err := search.ParseSearchRequest(r)
|
req, err := search.ParseSearchRequest(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if format == "html" || format == "" {
|
if format == "html" || format == "" {
|
||||||
pd := views.PageData{SourceURL: h.sourceURL, Query: q, Theme: h.getTheme(r)}
|
pd := views.PageData{SourceURL: h.sourceURL, Query: q, Theme: h.getTheme(r), FaviconService: h.getFaviconService(r)}
|
||||||
if views.IsHTMXRequest(r) {
|
if views.IsHTMXRequest(r) {
|
||||||
views.RenderSearchFragment(w, pd)
|
views.RenderSearchFragment(w, pd)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -118,7 +129,7 @@ func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
|
||||||
resp, err := h.searchSvc.Search(r.Context(), req)
|
resp, err := h.searchSvc.Search(r.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if req.Format == contracts.FormatHTML {
|
if req.Format == contracts.FormatHTML {
|
||||||
pd := views.PageData{SourceURL: h.sourceURL, Query: req.Query, Theme: h.getTheme(r)}
|
pd := views.PageData{SourceURL: h.sourceURL, Query: req.Query, Theme: h.getTheme(r), FaviconService: h.getFaviconService(r)}
|
||||||
if views.IsHTMXRequest(r) {
|
if views.IsHTMXRequest(r) {
|
||||||
views.RenderSearchFragment(w, pd)
|
views.RenderSearchFragment(w, pd)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -132,7 +143,7 @@ func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
if req.Format == contracts.FormatHTML {
|
if req.Format == contracts.FormatHTML {
|
||||||
pd := views.FromResponse(resp, req.Query, req.Pageno,
|
pd := views.FromResponse(resp, req.Query, req.Pageno,
|
||||||
r.FormValue("category"), r.FormValue("time"), r.FormValue("type"))
|
r.FormValue("category"), r.FormValue("time"), r.FormValue("type"), h.getFaviconService(r))
|
||||||
pd.Theme = h.getTheme(r)
|
pd.Theme = h.getTheme(r)
|
||||||
if err := views.RenderSearchAuto(w, r, pd); err != nil {
|
if err := views.RenderSearchAuto(w, r, pd); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
|
@ -182,6 +193,19 @@ func (h *Handler) Preferences(w http.ResponseWriter, r *http.Request) {
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// Persist favicon provider preference.
|
||||||
|
favicon := strings.TrimSpace(r.FormValue("favicon"))
|
||||||
|
switch favicon {
|
||||||
|
case "none", "google", "duckduckgo", "self":
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "favicon",
|
||||||
|
Value: favicon,
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 86400 * 365,
|
||||||
|
HttpOnly: false,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
http.Redirect(w, r, "/preferences", http.StatusFound)
|
http.Redirect(w, r, "/preferences", http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -192,7 +216,7 @@ func (h *Handler) Preferences(w http.ResponseWriter, r *http.Request) {
|
||||||
theme = cookie.Value
|
theme = cookie.Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := views.RenderPreferences(w, h.sourceURL, theme); err != nil {
|
if err := views.RenderPreferences(w, h.sourceURL, theme, h.getFaviconService(r)); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,17 +32,17 @@ import (
|
||||||
// mockUpstreamHandler returns controlled JSON responses.
|
// mockUpstreamHandler returns controlled JSON responses.
|
||||||
func mockUpstreamJSON(query string) contracts.SearchResponse {
|
func mockUpstreamJSON(query string) contracts.SearchResponse {
|
||||||
return contracts.SearchResponse{
|
return contracts.SearchResponse{
|
||||||
Query: query,
|
Query: query,
|
||||||
NumberOfResults: 2,
|
NumberOfResults: 2,
|
||||||
Results: []contracts.MainResult{
|
Results: []contracts.MainResult{
|
||||||
{Title: "Upstream Result 1", URL: ptr("https://upstream.example/1"), Content: "From upstream", Engine: "upstream"},
|
{Title: "Upstream Result 1", URL: ptr("https://upstream.example/1"), Content: "From upstream", Engine: "upstream"},
|
||||||
{Title: "Upstream Result 2", URL: ptr("https://upstream.example/2"), Content: "From upstream", Engine: "upstream"},
|
{Title: "Upstream Result 2", URL: ptr("https://upstream.example/2"), Content: "From upstream", Engine: "upstream"},
|
||||||
},
|
},
|
||||||
Answers: []map[string]any{},
|
Answers: []map[string]any{},
|
||||||
Corrections: []string{},
|
Corrections: []string{},
|
||||||
Infoboxes: []map[string]any{},
|
Infoboxes: []map[string]any{},
|
||||||
Suggestions: []string{"upstream suggestion"},
|
Suggestions: []string{"upstream suggestion"},
|
||||||
UnresponsiveEngines: [][2]string{},
|
UnresponsiveEngines: [][2]string{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,6 +74,7 @@ func newTestServer(t *testing.T) (*httptest.Server, *httpapi.Handler) {
|
||||||
mux.HandleFunc("/", h.Index)
|
mux.HandleFunc("/", h.Index)
|
||||||
mux.HandleFunc("/search", h.Search)
|
mux.HandleFunc("/search", h.Search)
|
||||||
mux.HandleFunc("/autocompleter", h.Autocompleter)
|
mux.HandleFunc("/autocompleter", h.Autocompleter)
|
||||||
|
mux.HandleFunc("/preferences", h.Preferences)
|
||||||
|
|
||||||
server := httptest.NewServer(mux)
|
server := httptest.NewServer(mux)
|
||||||
t.Cleanup(server.Close)
|
t.Cleanup(server.Close)
|
||||||
|
|
@ -228,3 +229,50 @@ func TestSearch_SourceURLInFooter(t *testing.T) {
|
||||||
t.Error("expected AGPLv3 link in footer")
|
t.Error("expected AGPLv3 link in footer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreferences_PostSetsFaviconCookie(t *testing.T) {
|
||||||
|
server, _ := newTestServer(t)
|
||||||
|
client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}}
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, server.URL+"/preferences", strings.NewReader("favicon=google&theme=dark"))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusFound {
|
||||||
|
t.Fatalf("expected redirect 302, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, c := range resp.Cookies() {
|
||||||
|
if c.Name == "favicon" {
|
||||||
|
found = true
|
||||||
|
if c.Value != "google" {
|
||||||
|
t.Fatalf("expected favicon cookie google, got %q", c.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("expected favicon cookie to be set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreferences_GetReflectsFaviconCookie(t *testing.T) {
|
||||||
|
server, _ := newTestServer(t)
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, server.URL+"/preferences", nil)
|
||||||
|
req.AddCookie(&http.Cookie{Name: "favicon", Value: "duckduckgo"})
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
html := string(body)
|
||||||
|
if !strings.Contains(html, `option value="duckduckgo" selected`) {
|
||||||
|
t.Fatalf("expected duckduckgo option selected, body: %s", html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,10 +74,10 @@
|
||||||
<p class="pref-desc">Fetch favicons for result URLs. "None" is most private.</p>
|
<p class="pref-desc">Fetch favicons for result URLs. "None" is most private.</p>
|
||||||
</div>
|
</div>
|
||||||
<select name="favicon" id="pref-favicon">
|
<select name="favicon" id="pref-favicon">
|
||||||
<option value="none" selected>None</option>
|
<option value="none" {{if eq .FaviconService "none"}}selected{{end}}>None</option>
|
||||||
<option value="google">Google</option>
|
<option value="google" {{if eq .FaviconService "google"}}selected{{end}}>Google</option>
|
||||||
<option value="duckduckgo">DuckDuckGo</option>
|
<option value="duckduckgo" {{if eq .FaviconService "duckduckgo"}}selected{{end}}>DuckDuckGo</option>
|
||||||
<option value="self">Self (Kafka)</option>
|
<option value="self" {{if eq .FaviconService "self"}}selected{{end}}>Self (Kafka)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.SafeTitle}}</a>
|
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.SafeTitle}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="result_url">
|
<div class="result_url">
|
||||||
{{if .Domain}}
|
{{if .FaviconIconURL}}
|
||||||
<img class="result-favicon" src="/favicon/{{.Domain}}" alt="" loading="lazy" width="14" height="14">
|
<img class="result-favicon" src="{{.FaviconIconURL}}" alt="" loading="lazy" width="14" height="14">
|
||||||
{{end}}
|
{{end}}
|
||||||
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.URL}}</a>
|
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.URL}}</a>
|
||||||
<span class="engine-badge" data-engine="{{.Engine}}">{{.Engine}}</span>
|
<span class="engine-badge" data-engine="{{.Engine}}">{{.Engine}}</span>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@
|
||||||
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.SafeTitle}}</a>
|
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.SafeTitle}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="result_url">
|
<div class="result_url">
|
||||||
|
{{if .FaviconIconURL}}
|
||||||
|
<img class="result-favicon" src="{{.FaviconIconURL}}" alt="" loading="lazy" width="14" height="14">
|
||||||
|
{{end}}
|
||||||
{{if .URL}}
|
{{if .URL}}
|
||||||
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.URL}}</a>
|
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.URL}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -56,16 +56,17 @@ type PageData struct {
|
||||||
ShowHeader bool
|
ShowHeader bool
|
||||||
IsImageSearch bool
|
IsImageSearch bool
|
||||||
// Theme is the user's selected theme (light/dark) from cookie
|
// Theme is the user's selected theme (light/dark) from cookie
|
||||||
Theme string
|
Theme string
|
||||||
|
FaviconService string
|
||||||
// New fields for three-column layout
|
// New fields for three-column layout
|
||||||
Categories []string
|
Categories []string
|
||||||
CategoryIcons map[string]string
|
CategoryIcons map[string]string
|
||||||
DisabledCategories []string
|
DisabledCategories []string
|
||||||
ActiveCategory string
|
ActiveCategory string
|
||||||
TimeFilters []FilterOption
|
TimeFilters []FilterOption
|
||||||
TypeFilters []FilterOption
|
TypeFilters []FilterOption
|
||||||
ActiveTime string
|
ActiveTime string
|
||||||
ActiveType string
|
ActiveType string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResultView is a template-friendly wrapper around a MainResult.
|
// ResultView is a template-friendly wrapper around a MainResult.
|
||||||
|
|
@ -76,6 +77,8 @@ type ResultView struct {
|
||||||
TemplateName string
|
TemplateName string
|
||||||
// Domain is the hostname extracted from the result URL, used for favicon proxying.
|
// Domain is the hostname extracted from the result URL, used for favicon proxying.
|
||||||
Domain string
|
Domain string
|
||||||
|
// FaviconIconURL is the resolved favicon image URL for the user's favicon preference (empty = hide).
|
||||||
|
FaviconIconURL string
|
||||||
// SafeTitle and SafeContent are HTML-unescaped versions for rendering.
|
// SafeTitle and SafeContent are HTML-unescaped versions for rendering.
|
||||||
// The API returns HTML entities which Go templates escape by default.
|
// The API returns HTML entities which Go templates escape by default.
|
||||||
SafeTitle template.HTML
|
SafeTitle template.HTML
|
||||||
|
|
@ -84,8 +87,8 @@ type ResultView struct {
|
||||||
|
|
||||||
// PageNumber represents a numbered pagination button.
|
// PageNumber represents a numbered pagination button.
|
||||||
type PageNumber struct {
|
type PageNumber struct {
|
||||||
Num int
|
Num int
|
||||||
IsCurrent bool
|
IsCurrent bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// InfoboxView is a template-friendly infobox.
|
// InfoboxView is a template-friendly infobox.
|
||||||
|
|
@ -102,9 +105,9 @@ type FilterOption struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
tmplFull *template.Template
|
tmplFull *template.Template
|
||||||
tmplIndex *template.Template
|
tmplIndex *template.Template
|
||||||
tmplFragment *template.Template
|
tmplFragment *template.Template
|
||||||
tmplPreferences *template.Template
|
tmplPreferences *template.Template
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -152,8 +155,26 @@ func OpenSearchXML(baseURL string) ([]byte, error) {
|
||||||
return []byte(result), nil
|
return []byte(result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// faviconIconURL returns a safe img src for the given service and hostname, or "" for none/invalid.
|
||||||
|
func faviconIconURL(service, domain string) string {
|
||||||
|
domain = strings.TrimSpace(domain)
|
||||||
|
if domain == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch service {
|
||||||
|
case "google":
|
||||||
|
return "https://www.google.com/s2/favicons?domain=" + url.QueryEscape(domain) + "&sz=32"
|
||||||
|
case "duckduckgo":
|
||||||
|
return "https://icons.duckduckgo.com/ip3/" + domain + ".ico"
|
||||||
|
case "self":
|
||||||
|
return "/favicon/" + domain
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FromResponse builds PageData from a search response and request params.
|
// FromResponse builds PageData from a search response and request params.
|
||||||
func FromResponse(resp contracts.SearchResponse, query string, pageno int, activeCategory, activeTime, activeType string) PageData {
|
func FromResponse(resp contracts.SearchResponse, query string, pageno int, activeCategory, activeTime, activeType, faviconService string) PageData {
|
||||||
// Set defaults
|
// Set defaults
|
||||||
if activeCategory == "" {
|
if activeCategory == "" {
|
||||||
activeCategory = "all"
|
activeCategory = "all"
|
||||||
|
|
@ -164,9 +185,10 @@ func FromResponse(resp contracts.SearchResponse, query string, pageno int, activ
|
||||||
Pageno: pageno,
|
Pageno: pageno,
|
||||||
NumberOfResults: resp.NumberOfResults,
|
NumberOfResults: resp.NumberOfResults,
|
||||||
UnresponsiveEngines: resp.UnresponsiveEngines,
|
UnresponsiveEngines: resp.UnresponsiveEngines,
|
||||||
|
FaviconService: faviconService,
|
||||||
|
|
||||||
// New: categories with icons
|
// New: categories with icons
|
||||||
Categories: []string{"all", "news", "images", "videos", "maps"},
|
Categories: []string{"all", "news", "images", "videos", "maps"},
|
||||||
DisabledCategories: []string{"shopping", "music", "weather"},
|
DisabledCategories: []string{"shopping", "music", "weather"},
|
||||||
CategoryIcons: map[string]string{
|
CategoryIcons: map[string]string{
|
||||||
"all": "🌐",
|
"all": "🌐",
|
||||||
|
|
@ -220,11 +242,12 @@ func FromResponse(resp contracts.SearchResponse, query string, pageno int, activ
|
||||||
}
|
}
|
||||||
r.Thumbnail = util.SanitizeResultURL(r.Thumbnail)
|
r.Thumbnail = util.SanitizeResultURL(r.Thumbnail)
|
||||||
pd.Results[i] = ResultView{
|
pd.Results[i] = ResultView{
|
||||||
MainResult: r,
|
MainResult: r,
|
||||||
TemplateName: tmplName,
|
TemplateName: tmplName,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
SafeTitle: template.HTML(html.UnescapeString(r.Title)),
|
FaviconIconURL: faviconIconURL(faviconService, domain),
|
||||||
SafeContent: template.HTML(html.UnescapeString(r.Content)),
|
SafeTitle: template.HTML(html.UnescapeString(r.Title)),
|
||||||
|
SafeContent: template.HTML(html.UnescapeString(r.Content)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -328,8 +351,12 @@ func RenderSearchAuto(w http.ResponseWriter, r *http.Request, data PageData) err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderPreferences renders the full preferences page.
|
// RenderPreferences renders the full preferences page.
|
||||||
func RenderPreferences(w http.ResponseWriter, sourceURL, theme string) error {
|
func RenderPreferences(w http.ResponseWriter, sourceURL, theme, faviconService string) error {
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
return tmplPreferences.ExecuteTemplate(w, "base", PageData{ShowHeader: true, SourceURL: sourceURL, Theme: theme})
|
return tmplPreferences.ExecuteTemplate(w, "base", PageData{
|
||||||
|
ShowHeader: true,
|
||||||
|
SourceURL: sourceURL,
|
||||||
|
Theme: theme,
|
||||||
|
FaviconService: faviconService,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package views
|
package views
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/metamorphosis-dev/samsa/internal/contracts"
|
"github.com/metamorphosis-dev/samsa/internal/contracts"
|
||||||
|
|
@ -37,7 +38,7 @@ func mockEmptyResponse() contracts.SearchResponse {
|
||||||
|
|
||||||
func TestFromResponse_Basic(t *testing.T) {
|
func TestFromResponse_Basic(t *testing.T) {
|
||||||
resp := mockSearchResponse("samsa trial", 42)
|
resp := mockSearchResponse("samsa trial", 42)
|
||||||
data := FromResponse(resp, "samsa trial", 1, "", "", "")
|
data := FromResponse(resp, "samsa trial", 1, "", "", "", "none")
|
||||||
|
|
||||||
if data.Query != "samsa trial" {
|
if data.Query != "samsa trial" {
|
||||||
t.Errorf("expected query 'samsa trial', got %q", data.Query)
|
t.Errorf("expected query 'samsa trial', got %q", data.Query)
|
||||||
|
|
@ -55,7 +56,7 @@ func TestFromResponse_Basic(t *testing.T) {
|
||||||
|
|
||||||
func TestFromResponse_Pagination(t *testing.T) {
|
func TestFromResponse_Pagination(t *testing.T) {
|
||||||
resp := mockSearchResponse("test", 100)
|
resp := mockSearchResponse("test", 100)
|
||||||
data := FromResponse(resp, "test", 3, "", "", "")
|
data := FromResponse(resp, "test", 3, "", "", "", "none")
|
||||||
|
|
||||||
if data.PrevPage != 2 {
|
if data.PrevPage != 2 {
|
||||||
t.Errorf("expected PrevPage 2, got %d", data.PrevPage)
|
t.Errorf("expected PrevPage 2, got %d", data.PrevPage)
|
||||||
|
|
@ -80,7 +81,7 @@ func TestFromResponse_Pagination(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFromResponse_Empty(t *testing.T) {
|
func TestFromResponse_Empty(t *testing.T) {
|
||||||
data := FromResponse(mockEmptyResponse(), "", 1, "", "", "")
|
data := FromResponse(mockEmptyResponse(), "", 1, "", "", "", "none")
|
||||||
|
|
||||||
if data.NumberOfResults != 0 {
|
if data.NumberOfResults != 0 {
|
||||||
t.Errorf("expected 0 results, got %d", data.NumberOfResults)
|
t.Errorf("expected 0 results, got %d", data.NumberOfResults)
|
||||||
|
|
@ -90,6 +91,31 @@ func TestFromResponse_Empty(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFromResponse_FaviconIconURL(t *testing.T) {
|
||||||
|
u := "https://example.com/path"
|
||||||
|
resp := contracts.SearchResponse{
|
||||||
|
Query: "q",
|
||||||
|
NumberOfResults: 1,
|
||||||
|
Results: []contracts.MainResult{{Title: "t", URL: &u, Engine: "bing"}},
|
||||||
|
Answers: []map[string]any{},
|
||||||
|
Corrections: []string{},
|
||||||
|
Infoboxes: []map[string]any{},
|
||||||
|
Suggestions: []string{},
|
||||||
|
UnresponsiveEngines: [][2]string{},
|
||||||
|
}
|
||||||
|
data := FromResponse(resp, "q", 1, "", "", "", "google")
|
||||||
|
if len(data.Results) != 1 {
|
||||||
|
t.Fatalf("expected 1 result, got %d", len(data.Results))
|
||||||
|
}
|
||||||
|
got := data.Results[0].FaviconIconURL
|
||||||
|
if got == "" || !strings.Contains(got, "google.com/s2/favicons") {
|
||||||
|
t.Fatalf("expected google favicon URL, got %q", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "example.com") {
|
||||||
|
t.Fatalf("expected domain in favicon URL, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestIsHTMXRequest(t *testing.T) {
|
func TestIsHTMXRequest(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue