main 5041353

favicon_discovery_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close