1package claudetool
2
3import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10)
11
12func TestOutputIframeRun(t *testing.T) {
13 // Create a temp directory for test files
14 tmpDir := t.TempDir()
15
16 // Create test HTML files
17 htmlFile := filepath.Join(tmpDir, "test.html")
18 if err := os.WriteFile(htmlFile, []byte("<html><head></head><body><h1>Hello</h1></body></html>"), 0o644); err != nil {
19 t.Fatal(err)
20 }
21
22 chartFile := filepath.Join(tmpDir, "chart.html")
23 if err := os.WriteFile(chartFile, []byte("<html><head></head><body><div>Chart</div></body></html>"), 0o644); err != nil {
24 t.Fatal(err)
25 }
26
27 // Create a data file
28 dataFile := filepath.Join(tmpDir, "data.json")
29 if err := os.WriteFile(dataFile, []byte(`{"values": [1, 2, 3]}`), 0o644); err != nil {
30 t.Fatal(err)
31 }
32
33 // Create a CSS file
34 cssFile := filepath.Join(tmpDir, "styles.css")
35 if err := os.WriteFile(cssFile, []byte("body { color: red; }"), 0o644); err != nil {
36 t.Fatal(err)
37 }
38
39 workingDir := &MutableWorkingDir{}
40 workingDir.Set(tmpDir)
41
42 tool := &OutputIframeTool{WorkingDir: workingDir}
43
44 tests := []struct {
45 name string
46 input map[string]any
47 wantErr bool
48 wantTitle string
49 wantFilename string
50 wantFiles int
51 checkHTML func(html string) bool
52 }{
53 {
54 name: "basic html file",
55 input: map[string]any{
56 "path": "test.html",
57 },
58 wantErr: false,
59 wantTitle: "",
60 wantFilename: "test.html",
61 wantFiles: 0,
62 },
63 {
64 name: "html with title",
65 input: map[string]any{
66 "path": "chart.html",
67 "title": "My Chart",
68 },
69 wantErr: false,
70 wantTitle: "My Chart",
71 wantFilename: "chart.html",
72 wantFiles: 0,
73 },
74 {
75 name: "html with data file",
76 input: map[string]any{
77 "path": "chart.html",
78 "title": "Chart with Data",
79 "files": map[string]any{
80 "data.json": "data.json",
81 },
82 },
83 wantErr: false,
84 wantTitle: "Chart with Data",
85 wantFilename: "chart.html",
86 wantFiles: 1,
87 checkHTML: func(html string) bool {
88 return strings.Contains(html, "window.__FILES__") &&
89 strings.Contains(html, "data.json")
90 },
91 },
92 {
93 name: "html with multiple files",
94 input: map[string]any{
95 "path": "chart.html",
96 "title": "Styled Chart",
97 "files": map[string]any{
98 "data.json": "data.json",
99 "styles.css": "styles.css",
100 },
101 },
102 wantErr: false,
103 wantTitle: "Styled Chart",
104 wantFilename: "chart.html",
105 wantFiles: 2,
106 checkHTML: func(html string) bool {
107 return strings.Contains(html, "window.__FILES__") &&
108 strings.Contains(html, "<style data-file=\"styles.css\">") &&
109 strings.Contains(html, "body { color: red; }")
110 },
111 },
112 {
113 name: "absolute path",
114 input: map[string]any{
115 "path": htmlFile,
116 },
117 wantErr: false,
118 wantFilename: "test.html",
119 wantFiles: 0,
120 },
121 {
122 name: "empty path",
123 input: map[string]any{
124 "path": "",
125 },
126 wantErr: true,
127 },
128 {
129 name: "missing path",
130 input: map[string]any{},
131 wantErr: true,
132 },
133 {
134 name: "nonexistent file",
135 input: map[string]any{
136 "path": "nonexistent.html",
137 },
138 wantErr: true,
139 },
140 {
141 name: "nonexistent data file",
142 input: map[string]any{
143 "path": "chart.html",
144 "files": map[string]any{
145 "missing.json": "missing.json",
146 },
147 },
148 wantErr: true,
149 },
150 }
151
152 for _, tt := range tests {
153 t.Run(tt.name, func(t *testing.T) {
154 inputJSON, err := json.Marshal(tt.input)
155 if err != nil {
156 t.Fatalf("failed to marshal input: %v", err)
157 }
158
159 result := tool.Run(context.Background(), inputJSON)
160
161 if tt.wantErr {
162 if result.Error == nil {
163 t.Error("expected error, got nil")
164 }
165 return
166 }
167
168 if result.Error != nil {
169 t.Errorf("unexpected error: %v", result.Error)
170 return
171 }
172
173 if len(result.LLMContent) != 1 || result.LLMContent[0].Text != "displayed" {
174 t.Errorf("expected LLMContent [displayed], got %v", result.LLMContent)
175 }
176
177 display, ok := result.Display.(OutputIframeDisplay)
178 if !ok {
179 t.Errorf("expected Display to be OutputIframeDisplay, got %T", result.Display)
180 return
181 }
182
183 if display.Type != "output_iframe" {
184 t.Errorf("expected Type 'output_iframe', got %q", display.Type)
185 }
186
187 if display.Title != tt.wantTitle {
188 t.Errorf("expected Title %q, got %q", tt.wantTitle, display.Title)
189 }
190
191 if display.Filename != tt.wantFilename {
192 t.Errorf("expected Filename %q, got %q", tt.wantFilename, display.Filename)
193 }
194
195 if len(display.Files) != tt.wantFiles {
196 t.Errorf("expected %d files, got %d", tt.wantFiles, len(display.Files))
197 }
198
199 if tt.checkHTML != nil && !tt.checkHTML(display.HTML) {
200 t.Errorf("HTML check failed, got: %s", display.HTML)
201 }
202 })
203 }
204}
205
206func TestDetectFileType(t *testing.T) {
207 tests := []struct {
208 filename string
209 want string
210 }{
211 {"data.json", "json"},
212 {"styles.css", "css"},
213 {"script.js", "js"},
214 {"data.csv", "csv"},
215 {"readme.txt", "text"},
216 {"unknown", "text"},
217 {"DATA.JSON", "json"}, // case insensitive
218 }
219
220 for _, tt := range tests {
221 t.Run(tt.filename, func(t *testing.T) {
222 got := detectFileType(tt.filename)
223 if got != tt.want {
224 t.Errorf("detectFileType(%q) = %q, want %q", tt.filename, got, tt.want)
225 }
226 })
227 }
228}
229
230func TestEscapeJSString(t *testing.T) {
231 tests := []struct {
232 input string
233 want string
234 }{
235 {"hello", "hello"},
236 {"hello\nworld", "hello\\nworld"},
237 {`say "hi"`, `say \"hi\"`},
238 {"back\\slash", "back\\\\slash"},
239 {"tab\there", "tab\\there"},
240 }
241
242 for _, tt := range tests {
243 t.Run(tt.input, func(t *testing.T) {
244 got := escapeJSString(tt.input)
245 if got != tt.want {
246 t.Errorf("escapeJSString(%q) = %q, want %q", tt.input, got, tt.want)
247 }
248 })
249 }
250}
251
252func TestInjectFiles(t *testing.T) {
253 tests := []struct {
254 name string
255 html string
256 files []EmbeddedFile
257 contains []string
258 }{
259 {
260 name: "no files",
261 html: "<html><head></head><body></body></html>",
262 files: nil,
263 contains: []string{"<html><head></head><body></body></html>"},
264 },
265 {
266 name: "inject json file",
267 html: "<html><head></head><body></body></html>",
268 files: []EmbeddedFile{
269 {Name: "data.json", Content: `{"x": 1}`, Type: "json"},
270 },
271 contains: []string{"window.__FILES__", "data.json"},
272 },
273 {
274 name: "inject css file",
275 html: "<html><head></head><body></body></html>",
276 files: []EmbeddedFile{
277 {Name: "styles.css", Content: "body { color: red; }", Type: "css"},
278 },
279 contains: []string{"<style data-file=\"styles.css\">", "body { color: red; }"},
280 },
281 {
282 name: "inject js file",
283 html: "<html><head></head><body></body></html>",
284 files: []EmbeddedFile{
285 {Name: "app.js", Content: "console.log('hi');", Type: "js"},
286 },
287 contains: []string{"<script data-file=\"app.js\">", "console.log('hi');"},
288 },
289 {
290 name: "html without head tag",
291 html: "<html><body>content</body></html>",
292 files: []EmbeddedFile{
293 {Name: "data.json", Content: `{}`, Type: "json"},
294 },
295 contains: []string{"<head>", "</head>", "window.__FILES__"},
296 },
297 {
298 name: "plain html without tags",
299 html: "<div>content</div>",
300 files: []EmbeddedFile{
301 {Name: "data.json", Content: `{}`, Type: "json"},
302 },
303 contains: []string{"window.__FILES__", "<div>content</div>"},
304 },
305 }
306
307 for _, tt := range tests {
308 t.Run(tt.name, func(t *testing.T) {
309 result := injectFiles(tt.html, tt.files)
310 for _, s := range tt.contains {
311 if !strings.Contains(result, s) {
312 t.Errorf("expected result to contain %q, got: %s", s, result)
313 }
314 }
315 })
316 }
317}
318
319func TestOutputIframeToolSchema(t *testing.T) {
320 workingDir := &MutableWorkingDir{}
321 workingDir.Set("/tmp")
322 tool := &OutputIframeTool{WorkingDir: workingDir}
323 llmTool := tool.Tool()
324
325 if llmTool.Name != "output_iframe" {
326 t.Errorf("expected name 'output_iframe', got %q", llmTool.Name)
327 }
328
329 if llmTool.Run == nil {
330 t.Error("expected Run function to be set")
331 }
332
333 if len(llmTool.InputSchema) == 0 {
334 t.Error("expected InputSchema to be set")
335 }
336
337 // Verify schema contains files property
338 if !strings.Contains(string(llmTool.InputSchema), "files") {
339 t.Error("expected InputSchema to contain 'files' property")
340 }
341}