browser_test.go
1 package tools 2 3 import ( 4 "context" 5 "strings" 6 "testing" 7 ) 8 9 func TestBrowser_Info(t *testing.T) { 10 tool := &BrowserTool{} 11 info := tool.Info() 12 13 if info.Name != "browser" { 14 t.Errorf("expected name 'browser', got %q", info.Name) 15 } 16 if len(info.Required) != 1 || info.Required[0] != "action" { 17 t.Errorf("expected required [action], got %v", info.Required) 18 } 19 20 props, ok := info.Parameters["properties"].(map[string]any) 21 if !ok { 22 t.Fatal("expected properties to be map[string]any") 23 } 24 25 expectedParams := []string{"action", "url", "selector", "text", "script", "timeout"} 26 for _, p := range expectedParams { 27 if _, exists := props[p]; !exists { 28 t.Errorf("expected parameter %q in properties", p) 29 } 30 } 31 } 32 33 func TestBrowser_RequiresApproval(t *testing.T) { 34 tool := &BrowserTool{} 35 if !tool.RequiresApproval() { 36 t.Error("expected RequiresApproval to return true") 37 } 38 } 39 40 func TestBrowser_InvalidJSON(t *testing.T) { 41 tool := &BrowserTool{} 42 result, err := tool.Run(context.Background(), `not valid json`) 43 if err != nil { 44 t.Fatalf("unexpected error: %v", err) 45 } 46 if !result.IsError { 47 t.Error("expected error result for invalid JSON") 48 } 49 if !contains(result.Content, "invalid arguments") { 50 t.Errorf("expected 'invalid arguments' in content, got: %s", result.Content) 51 } 52 } 53 54 func TestBrowser_MissingAction(t *testing.T) { 55 tool := &BrowserTool{} 56 result, err := tool.Run(context.Background(), `{}`) 57 if err != nil { 58 t.Fatalf("unexpected error: %v", err) 59 } 60 if !result.IsError { 61 t.Error("expected error result for missing action") 62 } 63 if !contains(result.Content, "missing required parameter: action") { 64 t.Errorf("expected missing action message, got: %s", result.Content) 65 } 66 } 67 68 func TestBrowser_UnknownAction(t *testing.T) { 69 tool := &BrowserTool{} 70 result, err := tool.Run(context.Background(), `{"action": "fly"}`) 71 if err != nil { 72 t.Fatalf("unexpected error: %v", err) 73 } 74 if !result.IsError { 75 t.Error("expected error result for unknown action") 76 } 77 if !contains(result.Content, "unknown action") { 78 t.Errorf("expected 'unknown action' in content, got: %s", result.Content) 79 } 80 } 81 82 func TestBrowser_NavigateMissingURL(t *testing.T) { 83 tool := &BrowserTool{} 84 result, err := tool.Run(context.Background(), `{"action": "navigate"}`) 85 if err != nil { 86 t.Fatalf("unexpected error: %v", err) 87 } 88 if !result.IsError { 89 t.Error("expected error result for navigate without URL") 90 } 91 if !contains(result.Content, "requires 'url'") { 92 t.Errorf("expected url required message, got: %s", result.Content) 93 } 94 } 95 96 func TestBrowser_ClickMissingSelector(t *testing.T) { 97 tool := &BrowserTool{} 98 result, err := tool.Run(context.Background(), `{"action": "click"}`) 99 if err != nil { 100 t.Fatalf("unexpected error: %v", err) 101 } 102 if !result.IsError { 103 t.Error("expected error result for click without selector") 104 } 105 if !contains(result.Content, "requires") { 106 t.Errorf("expected requires message, got: %s", result.Content) 107 } 108 } 109 110 func TestBrowser_TypeMissingSelector(t *testing.T) { 111 tool := &BrowserTool{} 112 result, err := tool.Run(context.Background(), `{"action": "type"}`) 113 if err != nil { 114 t.Fatalf("unexpected error: %v", err) 115 } 116 if !result.IsError { 117 t.Error("expected error result for type without selector") 118 } 119 if !contains(result.Content, "requires") { 120 t.Errorf("expected requires message, got: %s", result.Content) 121 } 122 } 123 124 func TestBrowser_WaitMissingSelector(t *testing.T) { 125 tool := &BrowserTool{} 126 result, err := tool.Run(context.Background(), `{"action": "wait"}`) 127 if err != nil { 128 t.Fatalf("unexpected error: %v", err) 129 } 130 if !result.IsError { 131 t.Error("expected error result for wait without selector") 132 } 133 if !contains(result.Content, "requires 'selector'") { 134 t.Errorf("expected selector required message, got: %s", result.Content) 135 } 136 } 137 138 func TestBrowser_ExecuteJSMissingScript(t *testing.T) { 139 tool := &BrowserTool{} 140 result, err := tool.Run(context.Background(), `{"action": "execute_js"}`) 141 if err != nil { 142 t.Fatalf("unexpected error: %v", err) 143 } 144 if !result.IsError { 145 t.Error("expected error result for execute_js without script") 146 } 147 if !contains(result.Content, "requires 'script'") { 148 t.Errorf("expected script required message, got: %s", result.Content) 149 } 150 } 151 152 func TestBrowser_CloseWhenNotRunning(t *testing.T) { 153 tool := &BrowserTool{} 154 result, err := tool.Run(context.Background(), `{"action": "close"}`) 155 if err != nil { 156 t.Fatalf("unexpected error: %v", err) 157 } 158 if result.IsError { 159 t.Errorf("expected no error when closing non-running browser, got: %s", result.Content) 160 } 161 if !contains(result.Content, "not running") { 162 t.Errorf("expected 'not running' message, got: %s", result.Content) 163 } 164 } 165 166 func TestValidatePageContent(t *testing.T) { 167 tests := []struct { 168 name string 169 content string 170 want bool // true = content is empty 171 }{ 172 {"empty string", "", true}, 173 {"whitespace only", " \n\t ", true}, 174 {"valid content", "Hello world", false}, 175 {"short but valid", "Please verify", false}, 176 } 177 178 for _, tt := range tests { 179 t.Run(tt.name, func(t *testing.T) { 180 got := isPageContentEmpty(tt.content) 181 if got != tt.want { 182 t.Errorf("isPageContentEmpty(%q) = %v, want %v", tt.content, got, tt.want) 183 } 184 }) 185 } 186 } 187 188 func TestBrowser_InfoDescription(t *testing.T) { 189 tool := &BrowserTool{} 190 info := tool.Info() 191 if !contains(info.Description, "isolated profile") { 192 t.Errorf("expected description to mention isolated profile, got: %s", info.Description) 193 } 194 } 195 196 func TestFormatNavigateResult(t *testing.T) { 197 tests := []struct { 198 name string 199 url string 200 title string 201 preview string 202 wantPreview bool 203 wantWarning bool 204 }{ 205 { 206 name: "normal page with content", 207 url: "https://jd.com", 208 title: "京东首页", 209 preview: "京东JD.COM-专业的综合网上购物商城", 210 wantPreview: true, 211 wantWarning: false, 212 }, 213 { 214 name: "anti-bot page", 215 url: "https://jd.com", 216 title: "请验证您的身份", 217 preview: "", 218 wantPreview: false, 219 wantWarning: true, 220 }, 221 { 222 name: "empty preview", 223 url: "https://example.com", 224 title: "Example", 225 preview: "", 226 wantPreview: false, 227 wantWarning: false, 228 }, 229 { 230 name: "long preview truncated", 231 url: "https://example.com", 232 title: "Example", 233 preview: strings.Repeat("あ", 250), // multi-byte chars 234 wantPreview: true, 235 wantWarning: false, 236 }, 237 } 238 239 for _, tt := range tests { 240 t.Run(tt.name, func(t *testing.T) { 241 result := formatNavigateResult(tt.url, tt.title, tt.preview) 242 if tt.wantPreview && !strings.Contains(result, "Preview:") { 243 t.Error("expected Preview in result") 244 } 245 if !tt.wantPreview && strings.Contains(result, "Preview:") { 246 t.Error("unexpected Preview in result") 247 } 248 if tt.wantWarning && !strings.Contains(result, "WARNING") { 249 t.Error("expected WARNING in result") 250 } 251 if !tt.wantWarning && strings.Contains(result, "WARNING") { 252 t.Error("unexpected WARNING in result") 253 } 254 }) 255 } 256 } 257 258 func TestFormatNavigateResult_UTF8Safe(t *testing.T) { 259 // Verify multi-byte rune truncation doesn't produce invalid UTF-8 260 preview := strings.Repeat("中", 300) // 300 Chinese chars, each 3 bytes 261 result := formatNavigateResult("https://example.com", "Test", preview) 262 if !strings.Contains(result, "Preview:") { 263 t.Fatal("expected preview in result") 264 } 265 // Verify the result is valid UTF-8 (strings.Contains would panic on invalid) 266 if !strings.Contains(result, "...") { 267 t.Error("expected truncation marker") 268 } 269 } 270 271 func TestDetectAntiBotPage(t *testing.T) { 272 tests := []struct { 273 title string 274 want bool 275 }{ 276 {"京东-综合网购首选-正品低价", false}, 277 {"Google", false}, 278 {"请验证您的身份", true}, 279 {"Just a moment...", true}, 280 {"Verify you are human", true}, 281 {"Access Denied", true}, 282 {"Attention Required! | Cloudflare", true}, 283 {"Are you a robot?", true}, 284 {"Security Check", true}, 285 {"Please wait while we verify", true}, 286 {"Robot Check", true}, 287 {"", false}, 288 } 289 290 for _, tt := range tests { 291 t.Run(tt.title, func(t *testing.T) { 292 got := detectAntiBotPage(tt.title) 293 if got != tt.want { 294 t.Errorf("detectAntiBotPage(%q) = %v, want %v", tt.title, got, tt.want) 295 } 296 }) 297 } 298 }