/ internal / tools / browser_test.go
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  }