1package cef
2
3import (
4 "context"
5 "testing"
6
7 "github.com/bnema/dumber/internal/application/port"
8 purecef "github.com/bnema/purego-cef/cef"
9 cefmocks "github.com/bnema/purego-cef/cef/mocks"
10 "github.com/stretchr/testify/mock"
11 "github.com/stretchr/testify/require"
12)
13
14func TestFaviconDiscoveryResolvesLinksAndFallback(t *testing.T) {
15 pageURL := "https://example.com/docs/page.html"
16 html := `<html><head>
17 <link rel="stylesheet" href="/style.css">
18 <link rel="icon" href="/favicon.png">
19 <link rel="shortcut icon" href="icons/shortcut.ico">
20 <link rel="apple-touch-icon" href="https://cdn.example.com/apple.png">
21 <link rel="icon" href="javascript:alert(1)">
22 </head></html>`
23
24 got := DiscoverFaviconCandidates(pageURL, html)
25 require.Equal(t, []string{
26 "https://example.com/favicon.png",
27 "https://example.com/docs/icons/shortcut.ico",
28 "https://cdn.example.com/apple.png",
29 "https://example.com/favicon.ico",
30 }, got)
31}
32
33func TestFaviconDiscoveryFallbackOnly(t *testing.T) {
34 require.Equal(t, []string{"https://example.com/favicon.ico"}, DiscoverFaviconCandidates("https://example.com/path", "<html></html>"))
35 require.Nil(t, DiscoverFaviconCandidates("about:blank", "<html></html>"))
36}
37
38func TestFaviconDiscoveryKeepsAbsoluteIconsForOpaquePageURL(t *testing.T) {
39 got := DiscoverFaviconCandidates("about:blank", `<link rel="icon" href="https://cdn.example.com/icon.png">`)
40 require.Equal(t, []string{"https://cdn.example.com/icon.png"}, got)
41}
42
43func TestFaviconDiscoveryIgnoresUnsafeIconURLs(t *testing.T) {
44 got := DiscoverFaviconCandidates("https://example.com/path", `<link rel="icon" href="data:image/png;base64,AAAA"><link rel="icon" href="//cdn.example.com/icon.png">`)
45 require.Equal(t, []string{"https://cdn.example.com/icon.png", "https://example.com/favicon.ico"}, got)
46}
47
48func TestFaviconDiscoveryIgnoresWhitespaceOnlyHrefs(t *testing.T) {
49 got := DiscoverFaviconCandidates("https://example.com/path", `<link rel="icon" href=" ">`)
50 require.Equal(t, []string{"https://example.com/favicon.ico"}, got)
51}
52
53func TestCEFOnFaviconUrlchangeForwardsOrderedCandidates(t *testing.T) {
54 prevDecode := decodeCEFStringList
55 decodeCEFStringList = func(list purecef.StringList) []string {
56 require.Equal(t, purecef.StringList(42), list)
57 return []string{"/favicon.png", "https://cdn.example.com/icon.ico", "javascript:alert(1)"}
58 }
59 defer func() { decodeCEFStringList = prevDecode }()
60
61 var page string
62 var icons []string
63 browser := cefmocks.NewMockBrowser(t)
64 browser.EXPECT().GetIdentifier().Return(int32(7)).Maybe()
65 wv := &WebView{ctx: context.Background(), uri: "https://example.com/docs/page", browser: browser, documentSeq: 1, callbacks: &port.WebViewCallbacks{
66 OnFaviconURLChanged: func(p string, urls []string) { page, icons = p, urls },
67 }}
68
69 (&handlerSet{wv: wv}).OnFaviconUrlchange(browser, purecef.StringList(42))
70
71 require.Equal(t, "https://example.com/docs/page", page)
72 require.Equal(t, []string{"https://example.com/favicon.png", "https://cdn.example.com/icon.ico"}, icons)
73}
74
75func TestCEFOnFaviconUrlchangeResolvesInternalCandidatesAgainstCommittedURL(t *testing.T) {
76 prevDecode := decodeCEFStringList
77 decodeCEFStringList = func(purecef.StringList) []string { return []string{"/favicon.png"} }
78 defer func() { decodeCEFStringList = prevDecode }()
79
80 var page string
81 var icons []string
82 browser := cefmocks.NewMockBrowser(t)
83 browser.EXPECT().GetIdentifier().Return(int32(7)).Maybe()
84 wv := &WebView{ctx: context.Background(), browser: browser, documentSeq: 1, callbacks: &port.WebViewCallbacks{
85 OnFaviconURLChanged: func(p string, urls []string) { page, icons = p, urls },
86 }}
87 wv.setCommittedURLsLocked("https://dumber.invalid/history", "dumb://history")
88
89 (&handlerSet{wv: wv}).OnFaviconUrlchange(browser, purecef.StringList(1))
90
91 require.Equal(t, "dumb://history", page)
92 require.Equal(t, []string{"https://dumber.invalid/favicon.png"}, icons)
93}
94
95func TestCEFLoadingFinishedDiscoversMetadataBeforeFallback(t *testing.T) {
96 prevNewStringVisitor := cefNewStringVisitor
97 cefNewStringVisitor = func(visitor purecef.StringVisitor) purecef.StringVisitor { return visitor }
98 defer func() { cefNewStringVisitor = prevNewStringVisitor }()
99
100 browser := cefmocks.NewMockBrowser(t)
101 frame := cefmocks.NewMockFrame(t)
102 browser.EXPECT().GetIdentifier().Return(int32(7)).Maybe()
103 browser.EXPECT().GetMainFrame().Return(frame)
104 frame.EXPECT().GetSource(mock.Anything).Run(func(visitor purecef.StringVisitor) {
105 visitor.Visit(`<html><head><link rel="icon" href="/meta.png"></head></html>`)
106 }).Return()
107
108 var page string
109 var icons []string
110 wv := &WebView{ctx: context.Background(), uri: "https://example.com/docs/page", browser: browser, documentSeq: 1, callbacks: &port.WebViewCallbacks{
111 OnFaviconURLChanged: func(p string, urls []string) { page, icons = p, urls },
112 }}
113
114 (&handlerSet{wv: wv}).OnLoadingStateChange(browser, 0, 1, 0)
115
116 require.Equal(t, "https://example.com/docs/page", page)
117 require.Equal(t, []string{"https://example.com/meta.png", "https://example.com/favicon.ico"}, icons)
118}
119
120func TestCEFLoadingFinishedResolvesInternalMetadataAgainstCommittedURL(t *testing.T) {
121 prevNewStringVisitor := cefNewStringVisitor
122 cefNewStringVisitor = func(visitor purecef.StringVisitor) purecef.StringVisitor { return visitor }
123 defer func() { cefNewStringVisitor = prevNewStringVisitor }()
124
125 browser := cefmocks.NewMockBrowser(t)
126 frame := cefmocks.NewMockFrame(t)
127 browser.EXPECT().GetIdentifier().Return(int32(7)).Maybe()
128 browser.EXPECT().GetMainFrame().Return(frame)
129 frame.EXPECT().GetSource(mock.Anything).Run(func(visitor purecef.StringVisitor) {
130 visitor.Visit(`<html><head><link rel="icon" href="/meta.png"></head></html>`)
131 }).Return()
132
133 var page string
134 var icons []string
135 wv := &WebView{ctx: context.Background(), browser: browser, documentSeq: 1, callbacks: &port.WebViewCallbacks{
136 OnFaviconURLChanged: func(p string, urls []string) { page, icons = p, urls },
137 }}
138 wv.setCommittedURLsLocked("https://dumber.invalid/history", "dumb://history")
139
140 (&handlerSet{wv: wv}).OnLoadingStateChange(browser, 0, 1, 0)
141
142 require.Equal(t, "dumb://history", page)
143 require.Equal(t, []string{"https://dumber.invalid/meta.png", "https://dumber.invalid/favicon.ico"}, icons)
144}
145
146func TestCEFLoadingFinishedForwardsFallbackCandidate(t *testing.T) {
147 var page string
148 var icons []string
149 wv := &WebView{ctx: context.Background(), uri: "https://example.com/docs/page", documentSeq: 1, callbacks: &port.WebViewCallbacks{
150 OnFaviconURLChanged: func(p string, urls []string) { page, icons = p, urls },
151 }}
152
153 (&handlerSet{wv: wv}).OnLoadingStateChange(nil, 0, 1, 0)
154
155 require.Equal(t, "https://example.com/docs/page", page)
156 require.Equal(t, []string{"https://example.com/favicon.ico"}, icons)
157}
158
159func TestCEFLoadingFinishedSkipsSourceWhenEngineProvidedCandidates(t *testing.T) {
160 prevDecode := decodeCEFStringList
161 decodeCEFStringList = func(purecef.StringList) []string { return []string{"/engine.png"} }
162 defer func() { decodeCEFStringList = prevDecode }()
163
164 browser := cefmocks.NewMockBrowser(t)
165 browser.EXPECT().GetIdentifier().Return(int32(7)).Maybe()
166 calls := 0
167 wv := &WebView{ctx: context.Background(), uri: "https://example.com/page", browser: browser, documentSeq: 1, callbacks: &port.WebViewCallbacks{
168 OnFaviconURLChanged: func(string, []string) { calls++ },
169 }}
170 h := &handlerSet{wv: wv}
171 h.OnFaviconUrlchange(browser, purecef.StringList(1))
172 h.OnLoadingStateChange(browser, 0, 0, 0)
173
174 require.Equal(t, 1, calls)
175}
176
177func TestCEFLoadingFinishedCoalescesPendingSourceDiscovery(t *testing.T) {
178 prevNewStringVisitor := cefNewStringVisitor
179 cefNewStringVisitor = func(visitor purecef.StringVisitor) purecef.StringVisitor { return visitor }
180 defer func() { cefNewStringVisitor = prevNewStringVisitor }()
181
182 browser := cefmocks.NewMockBrowser(t)
183 frame := cefmocks.NewMockFrame(t)
184 browser.EXPECT().GetMainFrame().Return(frame).Once()
185 frame.EXPECT().GetSource(mock.Anything).Return().Once()
186 wv := &WebView{ctx: context.Background(), uri: "https://example.com/page", documentSeq: 1, callbacks: &port.WebViewCallbacks{
187 OnFaviconURLChanged: func(string, []string) {},
188 }}
189 h := &handlerSet{wv: wv}
190 h.OnLoadingStateChange(browser, 0, 0, 0)
191 h.OnLoadingStateChange(browser, 0, 0, 0)
192}
193
194func TestCEFLoadingFinishedDoesNotRepeatCompletedSourceDiscovery(t *testing.T) {
195 prevNewStringVisitor := cefNewStringVisitor
196 var visitor purecef.StringVisitor
197 cefNewStringVisitor = func(v purecef.StringVisitor) purecef.StringVisitor {
198 visitor = v
199 return v
200 }
201 defer func() { cefNewStringVisitor = prevNewStringVisitor }()
202
203 browser := cefmocks.NewMockBrowser(t)
204 frame := cefmocks.NewMockFrame(t)
205 browser.EXPECT().GetIdentifier().Return(int32(7)).Maybe()
206 browser.EXPECT().GetMainFrame().Return(frame).Once()
207 frame.EXPECT().GetSource(mock.Anything).Return().Once()
208 wv := &WebView{ctx: context.Background(), uri: "https://example.com/page", documentSeq: 1, callbacks: &port.WebViewCallbacks{
209 OnFaviconURLChanged: func(string, []string) {},
210 }}
211 h := &handlerSet{wv: wv}
212
213 h.OnLoadingStateChange(browser, 0, 0, 0)
214 require.NotNil(t, visitor, "loading-finished must start source discovery")
215 visitor.Visit(`<html><head><link rel="icon" href="/meta.png"></head></html>`)
216
217 // The same document finishing again must not repeat the full-document
218 // transfer; the GetMainFrame and GetSource expectations are Once.
219 h.OnLoadingStateChange(browser, 0, 0, 0)
220}