test(omnibox): cover bang detection and navigation
- commit
- f1f04613360e2e4249830db87837a22e774b3670
- author
- brice · · 2025-12-28 14:31 UTC
- parent
- a934acc
- signature
- unsigned
2 files · +99 -20
M internal/ui/component/omnibox.go +44 -20
| @@ -809,27 +809,58 @@ func buildBangSuggestions(shortcuts map[string]config.SearchShortcut, query stri | |||
| 809 | 809 | return suggestions | |
| 810 | 810 | } | |
| 811 | 811 | ||
| 812 | + | func detectBangKey(shortcuts map[string]config.SearchShortcut, query string) string { | |
| 813 | + | spaceIdx := strings.Index(query, " ") | |
| 814 | + | if !strings.HasPrefix(query, "!") || spaceIdx <= 1 { | |
| 815 | + | return "" | |
| 816 | + | } | |
| 817 | + | ||
| 818 | + | candidate := query[1:spaceIdx] | |
| 819 | + | for key := range shortcuts { | |
| 820 | + | if strings.EqualFold(key, candidate) { | |
| 821 | + | return key | |
| 822 | + | } | |
| 823 | + | } | |
| 824 | + | ||
| 825 | + | return "" | |
| 826 | + | } | |
| 827 | + | ||
| 828 | + | func normalizeBangKey(shortcuts map[string]config.SearchShortcut, shortcutKey string) (string, bool) { | |
| 829 | + | for key := range shortcuts { | |
| 830 | + | if strings.EqualFold(key, shortcutKey) { | |
| 831 | + | return key, true | |
| 832 | + | } | |
| 833 | + | } | |
| 834 | + | return "", false | |
| 835 | + | } | |
| 836 | + | ||
| 837 | + | func buildBangNavigationText(shortcuts map[string]config.SearchShortcut, entryText string) string { | |
| 838 | + | shortcutKey, query, found := url.ParseBangShortcut(entryText) | |
| 839 | + | if !found || query == "" { | |
| 840 | + | return "" | |
| 841 | + | } | |
| 842 | + | ||
| 843 | + | resolvedKey, ok := normalizeBangKey(shortcuts, shortcutKey) | |
| 844 | + | if !ok { | |
| 845 | + | return "" | |
| 846 | + | } | |
| 847 | + | ||
| 848 | + | return "!" + resolvedKey + " " + query | |
| 849 | + | } | |
| 850 | + | ||
| 812 | 851 | func (o *Omnibox) loadBangSuggestions(query string) { | |
| 813 | 852 | suggestions := buildBangSuggestions(o.shortcuts, query) | |
| 814 | 853 | o.idleAddUpdateBangSuggestions(suggestions) | |
| 815 | 854 | } | |
| 816 | 855 | ||
| 817 | 856 | func (o *Omnibox) updateBangDetection(query string) { | |
| 818 | - | spaceIdx := strings.Index(query, " ") | |
| 819 | - | if !strings.HasPrefix(query, "!") || spaceIdx <= 1 { | |
| 857 | + | key := detectBangKey(o.shortcuts, query) | |
| 858 | + | if key == "" { | |
| 820 | 859 | o.clearDetectedBang() | |
| 821 | 860 | return | |
| 822 | 861 | } | |
| 823 | 862 | ||
| 824 | - | candidate := query[1:spaceIdx] | |
| 825 | - | for key := range o.shortcuts { | |
| 826 | - | if strings.EqualFold(key, candidate) { | |
| 827 | - | o.setDetectedBang(key) | |
| 828 | - | return | |
| 829 | - | } | |
| 830 | - | } | |
| 831 | - | ||
| 832 | - | o.clearDetectedBang() | |
| 863 | + | o.setDetectedBang(key) | |
| 833 | 864 | } | |
| 834 | 865 | ||
| 835 | 866 | func (o *Omnibox) clearBangState() { | |
| @@ -1240,15 +1271,8 @@ func (o *Omnibox) navigateToSelected() { | |||
| 1240 | 1271 | ||
| 1241 | 1272 | if bangMode { | |
| 1242 | 1273 | // If user typed a full bang query, navigate using the bang URL. | |
| 1243 | - | if shortcutKey, query, found := url.ParseBangShortcut(entryText); found { | |
| 1244 | - | resolvedKey := shortcutKey | |
| 1245 | - | for key := range o.shortcuts { | |
| 1246 | - | if strings.EqualFold(key, shortcutKey) { | |
| 1247 | - | resolvedKey = key | |
| 1248 | - | break | |
| 1249 | - | } | |
| 1250 | - | } | |
| 1251 | - | targetURL := o.buildURL("!" + resolvedKey + " " + query) | |
| 1274 | + | if navText := buildBangNavigationText(o.shortcuts, entryText); navText != "" { | |
| 1275 | + | targetURL := o.buildURL(navText) | |
| 1252 | 1276 | if targetURL == "" { | |
| 1253 | 1277 | return | |
| 1254 | 1278 | } | |
| @@ -809,27 +809,58 @@ func buildBangSuggestions(shortcuts map[string]config.SearchShortcut, query stri | |||||
| 809 | return suggestions | 809 | return suggestions | ||
| 810 | } | 810 | } | ||
| 811 | 811 | ||||
| 812 | + | func detectBangKey(shortcuts map[string]config.SearchShortcut, query string) string { | |||
| 813 | + | spaceIdx := strings.Index(query, " ") | |||
| 814 | + | if !strings.HasPrefix(query, "!") || spaceIdx <= 1 { | |||
| 815 | + | return "" | |||
| 816 | + | } | |||
| 817 | + | ||||
| 818 | + | candidate := query[1:spaceIdx] | |||
| 819 | + | for key := range shortcuts { | |||
| 820 | + | if strings.EqualFold(key, candidate) { | |||
| 821 | + | return key | |||
| 822 | + | } | |||
| 823 | + | } | |||
| 824 | + | ||||
| 825 | + | return "" | |||
| 826 | + | } | |||
| 827 | + | ||||
| 828 | + | func normalizeBangKey(shortcuts map[string]config.SearchShortcut, shortcutKey string) (string, bool) { | |||
| 829 | + | for key := range shortcuts { | |||
| 830 | + | if strings.EqualFold(key, shortcutKey) { | |||
| 831 | + | return key, true | |||
| 832 | + | } | |||
| 833 | + | } | |||
| 834 | + | return "", false | |||
| 835 | + | } | |||
| 836 | + | ||||
| 837 | + | func buildBangNavigationText(shortcuts map[string]config.SearchShortcut, entryText string) string { | |||
| 838 | + | shortcutKey, query, found := url.ParseBangShortcut(entryText) | |||
| 839 | + | if !found || query == "" { | |||
| 840 | + | return "" | |||
| 841 | + | } | |||
| 842 | + | ||||
| 843 | + | resolvedKey, ok := normalizeBangKey(shortcuts, shortcutKey) | |||
| 844 | + | if !ok { | |||
| 845 | + | return "" | |||
| 846 | + | } | |||
| 847 | + | ||||
| 848 | + | return "!" + resolvedKey + " " + query | |||
| 849 | + | } | |||
| 850 | + | ||||
| 812 | func (o *Omnibox) loadBangSuggestions(query string) { | 851 | func (o *Omnibox) loadBangSuggestions(query string) { | ||
| 813 | suggestions := buildBangSuggestions(o.shortcuts, query) | 852 | suggestions := buildBangSuggestions(o.shortcuts, query) | ||
| 814 | o.idleAddUpdateBangSuggestions(suggestions) | 853 | o.idleAddUpdateBangSuggestions(suggestions) | ||
| 815 | } | 854 | } | ||
| 816 | 855 | ||||
| 817 | func (o *Omnibox) updateBangDetection(query string) { | 856 | func (o *Omnibox) updateBangDetection(query string) { | ||
| 818 | - | spaceIdx := strings.Index(query, " ") | 857 | + | key := detectBangKey(o.shortcuts, query) |
| 819 | - | if !strings.HasPrefix(query, "!") || spaceIdx <= 1 { | 858 | + | if key == "" { |
| 820 | o.clearDetectedBang() | 859 | o.clearDetectedBang() | ||
| 821 | return | 860 | return | ||
| 822 | } | 861 | } | ||
| 823 | 862 | ||||
| 824 | - | candidate := query[1:spaceIdx] | 863 | + | o.setDetectedBang(key) |
| 825 | - | for key := range o.shortcuts { | |||
| 826 | - | if strings.EqualFold(key, candidate) { | |||
| 827 | - | o.setDetectedBang(key) | |||
| 828 | - | return | |||
| 829 | - | } | |||
| 830 | - | } | |||
| 831 | - | ||||
| 832 | - | o.clearDetectedBang() | |||
| 833 | } | 864 | } | ||
| 834 | 865 | ||||
| 835 | func (o *Omnibox) clearBangState() { | 866 | func (o *Omnibox) clearBangState() { | ||
| @@ -1240,15 +1271,8 @@ func (o *Omnibox) navigateToSelected() { | |||||
| 1240 | 1271 | ||||
| 1241 | if bangMode { | 1272 | if bangMode { | ||
| 1242 | // If user typed a full bang query, navigate using the bang URL. | 1273 | // If user typed a full bang query, navigate using the bang URL. | ||
| 1243 | - | if shortcutKey, query, found := url.ParseBangShortcut(entryText); found { | 1274 | + | if navText := buildBangNavigationText(o.shortcuts, entryText); navText != "" { |
| 1244 | - | resolvedKey := shortcutKey | 1275 | + | targetURL := o.buildURL(navText) |
| 1245 | - | for key := range o.shortcuts { | |||
| 1246 | - | if strings.EqualFold(key, shortcutKey) { | |||
| 1247 | - | resolvedKey = key | |||
| 1248 | - | break | |||
| 1249 | - | } | |||
| 1250 | - | } | |||
| 1251 | - | targetURL := o.buildURL("!" + resolvedKey + " " + query) | |||
| 1252 | if targetURL == "" { | 1276 | if targetURL == "" { | ||
| 1253 | return | 1277 | return | ||
| 1254 | } | 1278 | } | ||
M internal/ui/component/omnibox_bang_test.go +55 -0
| @@ -71,3 +71,58 @@ func TestBuildBangSuggestions(t *testing.T) { | |||
| 71 | 71 | }) | |
| 72 | 72 | } | |
| 73 | 73 | } | |
| 74 | + | ||
| 75 | + | func TestDetectBangKey(t *testing.T) { | |
| 76 | + | shortcuts := map[string]config.SearchShortcut{ | |
| 77 | + | "gh": {URL: "https://github.com/search?q=%s"}, | |
| 78 | + | "ddg": {URL: "https://duckduckgo.com/?q=%s"}, | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | cases := []struct { | |
| 82 | + | name string | |
| 83 | + | query string | |
| 84 | + | want string | |
| 85 | + | }{ | |
| 86 | + | {name: "no bang prefix", query: "gh test", want: ""}, | |
| 87 | + | {name: "bang only has no space", query: "!gh", want: ""}, | |
| 88 | + | {name: "space at position 1", query: "! test", want: ""}, | |
| 89 | + | {name: "unknown bang", query: "!nope test", want: ""}, | |
| 90 | + | {name: "case-insensitive match", query: "!GH test", want: "gh"}, | |
| 91 | + | {name: "valid bang key", query: "!ddg test", want: "ddg"}, | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | for _, tc := range cases { | |
| 95 | + | t.Run(tc.name, func(t *testing.T) { | |
| 96 | + | if got := detectBangKey(shortcuts, tc.query); got != tc.want { | |
| 97 | + | t.Fatalf("detectBangKey(%q)=%q want=%q", tc.query, got, tc.want) | |
| 98 | + | } | |
| 99 | + | }) | |
| 100 | + | } | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | func TestBuildBangNavigationText(t *testing.T) { | |
| 104 | + | shortcuts := map[string]config.SearchShortcut{ | |
| 105 | + | "gh": {URL: "https://github.com/search?q=%s"}, | |
| 106 | + | "ddg": {URL: "https://duckduckgo.com/?q=%s"}, | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | cases := []struct { | |
| 110 | + | name string | |
| 111 | + | entryText string | |
| 112 | + | want string | |
| 113 | + | }{ | |
| 114 | + | {name: "not a bang shortcut", entryText: "example.com", want: ""}, | |
| 115 | + | {name: "bang key without query", entryText: "!gh ", want: ""}, | |
| 116 | + | {name: "unknown bang key", entryText: "!nope test", want: ""}, | |
| 117 | + | {name: "normalizes key case", entryText: "!GH dumber", want: "!gh dumber"}, | |
| 118 | + | {name: "keeps query unchanged", entryText: "!ddg some query", want: "!ddg some query"}, | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | for _, tc := range cases { | |
| 122 | + | t.Run(tc.name, func(t *testing.T) { | |
| 123 | + | if got := buildBangNavigationText(shortcuts, tc.entryText); got != tc.want { | |
| 124 | + | t.Fatalf("buildBangNavigationText(%q)=%q want=%q", tc.entryText, got, tc.want) | |
| 125 | + | } | |
| 126 | + | }) | |
| 127 | + | } | |
| 128 | + | } | |
| @@ -71,3 +71,58 @@ func TestBuildBangSuggestions(t *testing.T) { | |||||
| 71 | }) | 71 | }) | ||
| 72 | } | 72 | } | ||
| 73 | } | 73 | } | ||
| 74 | + | ||||
| 75 | + | func TestDetectBangKey(t *testing.T) { | |||
| 76 | + | shortcuts := map[string]config.SearchShortcut{ | |||
| 77 | + | "gh": {URL: "https://github.com/search?q=%s"}, | |||
| 78 | + | "ddg": {URL: "https://duckduckgo.com/?q=%s"}, | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | cases := []struct { | |||
| 82 | + | name string | |||
| 83 | + | query string | |||
| 84 | + | want string | |||
| 85 | + | }{ | |||
| 86 | + | {name: "no bang prefix", query: "gh test", want: ""}, | |||
| 87 | + | {name: "bang only has no space", query: "!gh", want: ""}, | |||
| 88 | + | {name: "space at position 1", query: "! test", want: ""}, | |||
| 89 | + | {name: "unknown bang", query: "!nope test", want: ""}, | |||
| 90 | + | {name: "case-insensitive match", query: "!GH test", want: "gh"}, | |||
| 91 | + | {name: "valid bang key", query: "!ddg test", want: "ddg"}, | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | for _, tc := range cases { | |||
| 95 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 96 | + | if got := detectBangKey(shortcuts, tc.query); got != tc.want { | |||
| 97 | + | t.Fatalf("detectBangKey(%q)=%q want=%q", tc.query, got, tc.want) | |||
| 98 | + | } | |||
| 99 | + | }) | |||
| 100 | + | } | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | func TestBuildBangNavigationText(t *testing.T) { | |||
| 104 | + | shortcuts := map[string]config.SearchShortcut{ | |||
| 105 | + | "gh": {URL: "https://github.com/search?q=%s"}, | |||
| 106 | + | "ddg": {URL: "https://duckduckgo.com/?q=%s"}, | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | cases := []struct { | |||
| 110 | + | name string | |||
| 111 | + | entryText string | |||
| 112 | + | want string | |||
| 113 | + | }{ | |||
| 114 | + | {name: "not a bang shortcut", entryText: "example.com", want: ""}, | |||
| 115 | + | {name: "bang key without query", entryText: "!gh ", want: ""}, | |||
| 116 | + | {name: "unknown bang key", entryText: "!nope test", want: ""}, | |||
| 117 | + | {name: "normalizes key case", entryText: "!GH dumber", want: "!gh dumber"}, | |||
| 118 | + | {name: "keeps query unchanged", entryText: "!ddg some query", want: "!ddg some query"}, | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | for _, tc := range cases { | |||
| 122 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 123 | + | if got := buildBangNavigationText(shortcuts, tc.entryText); got != tc.want { | |||
| 124 | + | t.Fatalf("buildBangNavigationText(%q)=%q want=%q", tc.entryText, got, tc.want) | |||
| 125 | + | } | |||
| 126 | + | }) | |||
| 127 | + | } | |||
| 128 | + | } | |||