main 5041353

browsing_contexts_test.go RawHistoryFolder

1package config
2
3import (
4	"os"
5	"path/filepath"
6	"strings"
7	"testing"
8
9	"github.com/spf13/viper"
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/require"
12)
13
14// --- Legacy popups → browsing_contexts transform tests ---
15
16func TestTransformLegacyPopupsToBrowsingContexts_BasicMapping(t *testing.T) {
17	transformer := NewLegacyConfigTransformer()
18
19	rawConfig := map[string]any{
20		"workspace": map[string]any{
21			"popups": map[string]any{
22				"behavior":         "tabbed",
23				"oauth_auto_close": false,
24			},
25		},
26	}
27
28	transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)
29
30	ws := rawConfig["workspace"].(map[string]any)
31	// Old key should be deleted.
32	_, hasOld := ws["popups"]
33	assert.False(t, hasOld, "popups key should be removed after transform")
34
35	// New key should contain the old values.
36	newCfg, hasNew := ws["browsing_contexts"].(map[string]any)
37	require.True(t, hasNew, "browsing_contexts key should exist")
38	assert.Equal(t, "tabbed", newCfg["behavior"])
39	assert.Equal(t, false, newCfg["oauth_auto_close"])
40}
41
42func TestTransformLegacyPopupsToBrowsingContexts_NewKeyAlreadyPresent(t *testing.T) {
43	transformer := NewLegacyConfigTransformer()
44
45	rawConfig := map[string]any{
46		"workspace": map[string]any{
47			"popups": map[string]any{
48				"behavior": "tabbed",
49			},
50			"browsing_contexts": map[string]any{
51				"behavior": "split",
52			},
53		},
54	}
55
56	transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)
57
58	ws := rawConfig["workspace"].(map[string]any)
59	// Old key should still exist (new key was already present, skip transform).
60	oldCfg, hasOld := ws["popups"].(map[string]any)
61	assert.True(t, hasOld, "popups key should remain when browsing_contexts already exists")
62	assert.Equal(t, "tabbed", oldCfg["behavior"])
63
64	// New key should NOT be overwritten.
65	newCfg, hasNew := ws["browsing_contexts"].(map[string]any)
66	require.True(t, hasNew, "browsing_contexts key should still exist")
67	assert.Equal(t, "split", newCfg["behavior"], "browsing_contexts should not be overwritten by legacy transform")
68}
69
70func TestTransformLegacyPopupsToBrowsingContexts_NoWorkspace(t *testing.T) {
71	transformer := NewLegacyConfigTransformer()
72	rawConfig := map[string]any{"other": "value"}
73	// Should not panic
74	transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)
75	assert.NotNil(t, rawConfig)
76}
77
78func TestTransformLegacyPopupsToBrowsingContexts_NoPopupsKey(t *testing.T) {
79	transformer := NewLegacyConfigTransformer()
80	rawConfig := map[string]any{
81		"workspace": map[string]any{
82			"new_pane_url": "about:blank",
83		},
84	}
85	transformer.TransformLegacyPopupsToBrowsingContexts(rawConfig)
86	ws := rawConfig["workspace"].(map[string]any)
87	assert.Equal(t, "about:blank", ws["new_pane_url"])
88	_, hasBC := ws["browsing_contexts"]
89	assert.False(t, hasBC, "browsing_contexts should not be created if popups absent")
90}
91
92// --- Default config writing emits browsing_contexts instead of popups ---
93
94func TestDefaultConfig_TOMLHasBrowsingContextsNotPopups(t *testing.T) {
95	cfg := DefaultConfig()
96
97	content, err := encodeConfigToTOML(cfg)
98	require.NoError(t, err)
99
100	// Should contain [workspace.browsing_contexts] section.
101	assert.Contains(t, content, "[workspace.browsing_contexts]",
102		"TOML output should contain browsing_contexts section")
103
104	// Should NOT contain [workspace.popups] section.
105	assert.NotContains(t, content, "[workspace.popups]",
106		"TOML output should NOT contain deprecated popups section")
107
108	// The section should have the expected keys.
109	assert.Contains(t, content, "behavior = ")
110	assert.Contains(t, content, "oauth_auto_close = ")
111}
112
113// --- Loader defaults expose browsing_contexts keys, NOT popups ---
114
115func TestSetWorkspaceDefaults_ExposesBrowsingContextsNotPopups(t *testing.T) {
116	mgr := &Manager{viper: viper.New()}
117	mgr.setDefaults()
118
119	// browsing_contexts defaults should be registered.
120	assert.Equal(t, "split", mgr.viper.GetString("workspace.browsing_contexts.behavior"))
121	assert.Equal(t, "right", mgr.viper.GetString("workspace.browsing_contexts.placement"))
122	assert.Equal(t, "stacked", mgr.viper.GetString("workspace.browsing_contexts.blank_target_behavior"))
123	assert.True(t, mgr.viper.GetBool("workspace.browsing_contexts.open_in_new_pane"))
124	assert.True(t, mgr.viper.GetBool("workspace.browsing_contexts.follow_pane_context"))
125	assert.True(t, mgr.viper.GetBool("workspace.browsing_contexts.enable_smart_detection"))
126	assert.True(t, mgr.viper.GetBool("workspace.browsing_contexts.oauth_auto_close"))
127
128	// popups defaults should NOT be registered.
129	assert.Empty(t, mgr.viper.GetString("workspace.popups.behavior"))
130	assert.Empty(t, mgr.viper.GetString("workspace.popups.placement"))
131	assert.Empty(t, mgr.viper.GetString("workspace.popups.blank_target_behavior"))
132	assert.False(t, mgr.viper.GetBool("workspace.popups.open_in_new_pane"))
133	assert.False(t, mgr.viper.GetBool("workspace.popups.follow_pane_context"))
134	assert.False(t, mgr.viper.GetBool("workspace.popups.enable_smart_detection"))
135	assert.False(t, mgr.viper.GetBool("workspace.popups.oauth_auto_close"))
136}
137
138func TestBindBrowsingContextEnvAliases_AcceptsLegacyPopupEnvNames(t *testing.T) {
139	t.Setenv("DUMBER_WORKSPACE_POPUPS_BEHAVIOR", "tabbed")
140
141	v := viper.New()
142	v.SetEnvPrefix("DUMBER")
143	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
144	v.AutomaticEnv()
145	v.SetDefault("workspace.browsing_contexts.behavior", "split")
146	require.NoError(t, bindBrowsingContextEnvAliases(v))
147
148	assert.Equal(t, "tabbed", v.GetString("workspace.browsing_contexts.behavior"))
149}
150
151// --- Validation error paths mention workspace.browsing_contexts, not workspace.popups ---
152
153func TestValidatePopups_ErrorMessagesUseBrowsingContexts(t *testing.T) {
154	tests := []struct {
155		name     string
156		mutate   func(*Config)
157		wantText string
158	}{
159		{
160			name: "invalid behavior",
161			mutate: func(cfg *Config) {
162				cfg.Workspace.BrowsingContexts.Behavior = PopupBehavior("bad")
163			},
164			wantText: "workspace.browsing_contexts.behavior",
165		},
166		{
167			name: "invalid placement",
168			mutate: func(cfg *Config) {
169				cfg.Workspace.BrowsingContexts.Behavior = PopupBehaviorSplit
170				cfg.Workspace.BrowsingContexts.Placement = "center"
171			},
172			wantText: "workspace.browsing_contexts.placement",
173		},
174		{
175			name: "invalid blank_target_behavior",
176			mutate: func(cfg *Config) {
177				cfg.Workspace.BrowsingContexts.BlankTargetBehavior = "popup"
178			},
179			wantText: "workspace.browsing_contexts.blank_target_behavior",
180		},
181	}
182
183	for _, tt := range tests {
184		t.Run(tt.name, func(t *testing.T) {
185			cfg := DefaultConfig()
186			// Sync Popups alias so both fields match (validation reads BrowsingContexts).
187			cfg.Workspace.Popups = cfg.Workspace.BrowsingContexts
188			tt.mutate(cfg)
189			cfg.Workspace.Popups = cfg.Workspace.BrowsingContexts
190
191			err := validateConfig(cfg)
192			require.Error(t, err)
193			assert.Contains(t, err.Error(), tt.wantText,
194				"validation error should reference browsing_contexts, not popups")
195
196			// Ensure it does NOT reference the old popups nomenclature.
197			assert.NotContains(t, err.Error(), "workspace.popups.",
198				"validation error should NOT reference deprecated workspace.popups")
199		})
200	}
201}
202
203func TestValidatePopups_CompatibilityAliasMirrorsBrowsingContexts(t *testing.T) {
204	// When BrowsingContexts is valid but Popups alias is stale,
205	// validation should still pass because it reads BrowsingContexts.
206	cfg := DefaultConfig()
207	// Deliberately make Popups stale.
208	cfg.Workspace.Popups.Behavior = PopupBehavior("invalid")
209	cfg.Workspace.Popups.OAuthAutoClose = !cfg.Workspace.BrowsingContexts.OAuthAutoClose
210
211	err := validateConfig(cfg)
212	require.NoError(t, err, "validation should read BrowsingContexts, not the stale Popups alias")
213}
214
215// --- Runtime alias sync (BrowsingContexts → Popups) ---
216
217func TestNormalizeBrowsingContexts_SyncsPopupsAlias(t *testing.T) {
218	cfg := DefaultConfig()
219
220	// Make BrowsingContexts and Popups diverge.
221	cfg.Workspace.BrowsingContexts.Behavior = PopupBehaviorTabbed
222	cfg.Workspace.Popups.Behavior = PopupBehaviorSplit
223
224	normalizeBrowsingContexts(cfg)
225
226	assert.Equal(t, PopupBehaviorTabbed, cfg.Workspace.Popups.Behavior,
227		"normalizeConfig should sync Popups ← BrowsingContexts")
228	assert.Equal(t, cfg.Workspace.BrowsingContexts.Placement, cfg.Workspace.Popups.Placement)
229	assert.Equal(t, cfg.Workspace.BrowsingContexts.OAuthAutoClose, cfg.Workspace.Popups.OAuthAutoClose)
230	assert.Equal(t, cfg.Workspace.BrowsingContexts.OpenInNewPane, cfg.Workspace.Popups.OpenInNewPane)
231}
232
233func TestDefaultConfig_PopupsMirrorsBrowsingContexts(t *testing.T) {
234	cfg := DefaultConfig()
235
236	// After DefaultConfig(), both fields should be identical.
237	assert.Equal(t, cfg.Workspace.BrowsingContexts.Behavior, cfg.Workspace.Popups.Behavior)
238	assert.Equal(t, cfg.Workspace.BrowsingContexts.Placement, cfg.Workspace.Popups.Placement)
239	assert.Equal(t, cfg.Workspace.BrowsingContexts.BlankTargetBehavior, cfg.Workspace.Popups.BlankTargetBehavior)
240	assert.Equal(t, cfg.Workspace.BrowsingContexts.OpenInNewPane, cfg.Workspace.Popups.OpenInNewPane)
241	assert.Equal(t, cfg.Workspace.BrowsingContexts.FollowPaneContext, cfg.Workspace.Popups.FollowPaneContext)
242	assert.Equal(t, cfg.Workspace.BrowsingContexts.EnableSmartDetection, cfg.Workspace.Popups.EnableSmartDetection)
243	assert.Equal(t, cfg.Workspace.BrowsingContexts.OAuthAutoClose, cfg.Workspace.Popups.OAuthAutoClose)
244}
245
246func TestSchemaProvider_UsesBrowsingContextsKeys(t *testing.T) {
247	provider := NewSchemaProvider()
248	schema := provider.GetSchema()
249
250	foundBrowsingContexts := false
251	foundPopups := false
252
253	for _, key := range schema {
254		if key.Key == "workspace.browsing_contexts.behavior" {
255			foundBrowsingContexts = true
256		}
257		if key.Key == "workspace.popups.behavior" {
258			foundPopups = true
259		}
260	}
261
262	assert.True(t, foundBrowsingContexts, "schema should include workspace.browsing_contexts.* keys")
263	assert.False(t, foundPopups, "schema should NOT include workspace.popups.* keys")
264}
265
266// --- End-to-end: legacy config round-trips through viper ---
267
268func TestLegacyPopupsFullRoundTrip(t *testing.T) {
269	// Simulate reading an old config file with [workspace.popups].
270	m := &Manager{viper: viper.New()}
271	m.viper.Set("workspace.popups.behavior", "stacked")
272	m.viper.Set("workspace.popups.placement", "left")
273	m.viper.Set("workspace.popups.oauth_auto_close", false)
274
275	// Transform should map popups → browsing_contexts.
276	settings, err := m.transformedSettings()
277	require.NoError(t, err)
278
279	// popups key should be gone from viper after transform.
280	assert.False(t, settings.IsSet("workspace.popups.behavior"),
281		"workspace.popups.behavior should not exist after transform")
282
283	// browsing_contexts should now hold the values.
284	assert.Equal(t, "stacked", settings.GetString("workspace.browsing_contexts.behavior"))
285	assert.Equal(t, "left", settings.GetString("workspace.browsing_contexts.placement"))
286	assert.False(t, settings.GetBool("workspace.browsing_contexts.oauth_auto_close"))
287
288	// Unmarshal should succeed.
289	cfg := &Config{}
290	err = settings.Unmarshal(cfg)
291	require.NoError(t, err)
292
293	// BrowsingContexts should have the legacy values.
294	assert.Equal(t, PopupBehaviorStacked, cfg.Workspace.BrowsingContexts.Behavior)
295	assert.Equal(t, "left", cfg.Workspace.BrowsingContexts.Placement)
296	assert.False(t, cfg.Workspace.BrowsingContexts.OAuthAutoClose)
297
298	// Popups should be empty (not unmarshaled, will be synced in normalizeConfig).
299	assert.Equal(t, PopupBehavior(""), cfg.Workspace.Popups.Behavior)
300
301	// After normalization, Popups should mirror BrowsingContexts.
302	normalizeBrowsingContexts(cfg)
303	assert.Equal(t, PopupBehaviorStacked, cfg.Workspace.Popups.Behavior)
304	assert.Equal(t, "left", cfg.Workspace.Popups.Placement)
305	assert.False(t, cfg.Workspace.Popups.OAuthAutoClose)
306}
307
308func TestLegacyPopupsFileOverridesBrowsingContextDefaults(t *testing.T) {
309	configFile := filepath.Join(t.TempDir(), "config.toml")
310	content := `
311[workspace.popups]
312behavior = "stacked"
313placement = "left"
314oauth_auto_close = false
315`
316	require.NoError(t, os.WriteFile(configFile, []byte(content), 0o644))
317
318	m := &Manager{viper: viper.New()}
319	m.viper.SetConfigFile(configFile)
320	m.setDefaults()
321	require.NoError(t, m.viper.ReadInConfig())
322
323	settings, err := m.transformedSettings()
324	require.NoError(t, err)
325
326	assert.Equal(t, "stacked", settings.GetString("workspace.browsing_contexts.behavior"))
327	assert.Equal(t, "left", settings.GetString("workspace.browsing_contexts.placement"))
328	assert.False(t, settings.GetBool("workspace.browsing_contexts.oauth_auto_close"))
329
330	cfg := &Config{}
331	require.NoError(t, settings.Unmarshal(cfg))
332	normalizeConfig(cfg)
333
334	assert.Equal(t, PopupBehaviorStacked, cfg.Workspace.BrowsingContexts.Behavior)
335	assert.Equal(t, "left", cfg.Workspace.BrowsingContexts.Placement)
336	assert.False(t, cfg.Workspace.BrowsingContexts.OAuthAutoClose)
337	assert.Equal(t, cfg.Workspace.BrowsingContexts, cfg.Workspace.Popups)
338}
339
340func TestLegacyPopupsFilePreservesCanonicalBrowsingContextsEnvOverride(t *testing.T) {
341	configFile := filepath.Join(t.TempDir(), "config.toml")
342	content := `
343[workspace.popups]
344behavior = "stacked"
345placement = "left"
346`
347	require.NoError(t, os.WriteFile(configFile, []byte(content), 0o644))
348	t.Setenv("DUMBER_WORKSPACE_BROWSING_CONTEXTS_BEHAVIOR", "tabbed")
349
350	m := &Manager{viper: viper.New()}
351	m.viper.SetConfigType("toml")
352	m.viper.SetConfigFile(configFile)
353	m.viper.SetEnvPrefix("DUMBER")
354	m.viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
355	m.viper.AutomaticEnv()
356	m.setDefaults()
357	require.NoError(t, m.viper.ReadInConfig())
358
359	settings, err := m.transformedSettings()
360	require.NoError(t, err)
361
362	assert.Equal(t, "tabbed", settings.GetString("workspace.browsing_contexts.behavior"))
363	assert.Equal(t, "left", settings.GetString("workspace.browsing_contexts.placement"))
364}
365
366func TestReload_ReappliesLegacyPopupsCompatibility(t *testing.T) {
367	configFile := filepath.Join(t.TempDir(), "config.toml")
368	writeConfig := func(content string) {
369		require.NoError(t, os.WriteFile(configFile, []byte(content), 0o644))
370	}
371
372	m := &Manager{viper: viper.New()}
373	m.viper.SetConfigType("toml")
374	m.viper.SetConfigFile(configFile)
375	m.viper.SetEnvPrefix("DUMBER")
376	m.viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
377	m.viper.AutomaticEnv()
378	m.setDefaults()
379
380	writeConfig(`
381[workspace.popups]
382behavior = "stacked"
383placement = "left"
384`)
385	require.NoError(t, m.reload())
386	require.Equal(t, PopupBehaviorStacked, m.config.Workspace.BrowsingContexts.Behavior)
387	require.Equal(t, "left", m.config.Workspace.BrowsingContexts.Placement)
388
389	writeConfig(`
390[workspace.popups]
391behavior = "tabbed"
392placement = "bottom"
393`)
394	require.NoError(t, m.reload())
395	assert.Equal(t, PopupBehaviorTabbed, m.config.Workspace.BrowsingContexts.Behavior)
396	assert.Equal(t, "bottom", m.config.Workspace.BrowsingContexts.Placement)
397	assert.Equal(t, m.config.Workspace.BrowsingContexts, m.config.Workspace.Popups)
398}
399
400func TestReload_MixedLegacyAndCanonicalBrowsingContexts_BackfillsMissingCanonicalFields(t *testing.T) {
401	configFile := filepath.Join(t.TempDir(), "config.toml")
402	require.NoError(t, os.WriteFile(configFile, []byte(`
403[workspace.popups]
404placement = "left"
405oauth_auto_close = false
406
407[workspace.browsing_contexts]
408behavior = "tabbed"
409`), 0o644))
410
411	m := &Manager{viper: viper.New()}
412	m.viper.SetConfigType("toml")
413	m.viper.SetConfigFile(configFile)
414	m.viper.SetEnvPrefix("DUMBER")
415	m.viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
416	m.viper.AutomaticEnv()
417	m.setDefaults()
418
419	require.NoError(t, m.reload())
420	assert.Equal(t, PopupBehaviorTabbed, m.config.Workspace.BrowsingContexts.Behavior)
421	assert.Equal(t, "left", m.config.Workspace.BrowsingContexts.Placement)
422	assert.False(t, m.config.Workspace.BrowsingContexts.OAuthAutoClose)
423	assert.Equal(t, m.config.Workspace.BrowsingContexts, m.config.Workspace.Popups)
424}
425
426func TestSave_WhenWatching_SyncsPopupsAliasBeforeSkipReload(t *testing.T) {
427	configFile := filepath.Join(t.TempDir(), "config.toml")
428	m := &Manager{viper: viper.New(), watching: true}
429	m.viper.SetConfigType("toml")
430	m.viper.SetConfigFile(configFile)
431	m.setDefaults()
432	m.config = DefaultConfig()
433
434	cfg := DefaultConfig()
435	cfg.Workspace.BrowsingContexts.Behavior = PopupBehaviorTabbed
436	cfg.Workspace.BrowsingContexts.Placement = "bottom"
437	cfg.Workspace.Popups.Behavior = PopupBehaviorSplit
438	cfg.Workspace.Popups.Placement = "left"
439
440	require.NoError(t, m.Save(cfg))
441	assert.True(t, m.skipNextReload)
442	assert.Equal(t, cfg.Workspace.BrowsingContexts.Behavior, m.config.Workspace.BrowsingContexts.Behavior)
443	assert.Equal(t, cfg.Workspace.BrowsingContexts.Placement, m.config.Workspace.BrowsingContexts.Placement)
444	assert.Equal(t, m.config.Workspace.BrowsingContexts, m.config.Workspace.Popups)
445}

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