1import { test, expect } from '@playwright/test';
2
3test.describe('Tool Component Verification', () => {
4 test('all tools use custom components, not GenericTool', async ({ page }) => {
5 await page.goto('/');
6 await page.waitForLoadState('domcontentloaded');
7
8 const messageInput = page.getByTestId('message-input');
9 const sendButton = page.getByTestId('send-button');
10
11 // Send the tool smorgasbord message to trigger all tool types
12 await messageInput.fill('tool smorgasbord');
13 await sendButton.click();
14
15 // Wait for the response text to appear
16 await page.waitForFunction(
17 () => document.body.textContent?.includes('Here\'s a sample of all the tools:') ?? false,
18 undefined,
19 { timeout: 30000 }
20 );
21
22 // Wait for all tool calls to complete
23 await page.waitForFunction(
24 () => document.querySelectorAll('[data-testid="tool-call-completed"]').length >= 9,
25 undefined,
26 { timeout: 30000 }
27 );
28
29 // Verify bash tool uses BashTool component (has bash-tool class)
30 const bashTool = page.locator('.bash-tool').first();
31 await expect(bashTool).toBeVisible();
32 await expect(bashTool.locator('.bash-tool-emoji')).toBeVisible();
33 await expect(bashTool.locator('.bash-tool-command')).toBeVisible();
34
35 // Verify think tool uses ThinkTool component (has tool class with think emoji)
36 const thinkTool = page.locator('.tool').filter({ hasText: 'I\'m thinking about the best approach' });
37 await expect(thinkTool.first()).toBeVisible();
38 await expect(thinkTool.locator('.tool-emoji').filter({ hasText: '💭' }).first()).toBeVisible();
39
40 // Verify patch tool uses PatchTool component (has patch-tool class)
41 const patchTool = page.locator('.patch-tool').first();
42 await expect(patchTool).toBeVisible();
43 await expect(patchTool.locator('.patch-tool-emoji')).toBeVisible();
44
45 // Verify screenshot tool uses ScreenshotTool component (has screenshot-tool class)
46 const screenshotTool = page.locator('.screenshot-tool').first();
47 await expect(screenshotTool).toBeVisible();
48 await expect(screenshotTool.locator('.screenshot-tool-emoji').filter({ hasText: '📷' })).toBeVisible();
49
50 // Verify keyword_search tool uses KeywordSearchTool component (has tool class with search emoji)
51 const keywordTool = page.locator('.tool').filter({ hasText: 'find all references' });
52 await expect(keywordTool.first()).toBeVisible();
53 await expect(keywordTool.locator('.tool-emoji').filter({ hasText: '🔍' }).first()).toBeVisible();
54
55 // Verify browser_navigate tool uses BrowserNavigateTool component (has tool class with globe emoji and URL)
56 const navigateTool = page.locator('.tool').filter({ hasText: 'https://example.com' });
57 await expect(navigateTool.first()).toBeVisible();
58 await expect(navigateTool.locator('.tool-emoji').filter({ hasText: '🌐' }).first()).toBeVisible();
59
60 // Verify browser_eval tool uses BrowserEvalTool component (has tool class with lightning emoji)
61 const evalTool = page.locator('.tool').filter({ hasText: 'document.title' });
62 await expect(evalTool.first()).toBeVisible();
63 await expect(evalTool.locator('.tool-emoji').filter({ hasText: '⚡' }).first()).toBeVisible();
64
65 // Verify read_image tool uses ReadImageTool component (has screenshot-tool class with frame emoji)
66 const readImageTool = page.locator('.screenshot-tool').filter({ hasText: '/tmp/image.png' });
67 await expect(readImageTool.first()).toBeVisible();
68 await expect(readImageTool.locator('.screenshot-tool-emoji').filter({ hasText: '🖼️' }).first()).toBeVisible();
69
70 // Verify browser_recent_console_logs tool uses BrowserConsoleLogsTool component (has tool class with clipboard emoji)
71 const consoleTool = page.locator('.tool').filter({ hasText: 'console logs' });
72 await expect(consoleTool.first()).toBeVisible();
73 await expect(consoleTool.locator('.tool-emoji').filter({ hasText: '📋' }).first()).toBeVisible();
74
75 // CRITICAL: Verify that GenericTool (gear emoji ⚙️) is NOT used for any of these tools
76 // We check that NO tool has the generic gear icon
77 const genericToolGearEmojis = page.locator('.tool-emoji').filter({ hasText: '⚙️' });
78 expect(await genericToolGearEmojis.count()).toBe(0);
79 });
80
81 test('bash tool shows command in header', async ({ page }) => {
82 await page.goto('/');
83 await page.waitForLoadState('domcontentloaded');
84
85 const messageInput = page.getByTestId('message-input');
86 const sendButton = page.getByTestId('send-button');
87
88 await messageInput.fill('bash: unique-test-command-xyz123');
89 await sendButton.click();
90
91 // Wait for and verify the specific bash tool we just created
92 await page.waitForFunction(
93 () => document.body.textContent?.includes('unique-test-command-xyz123') ?? false,
94 undefined,
95 { timeout: 30000 }
96 );
97
98 // Verify bash tool shows the command in the header (collapsed state)
99 const bashToolWithOurCommand = page.locator('.bash-tool').filter({ hasText: 'unique-test-command-xyz123' });
100 await expect(bashToolWithOurCommand).toBeVisible();
101 const commandElement = bashToolWithOurCommand.locator('.bash-tool-command');
102 await expect(commandElement).toBeVisible();
103 const commandText = await commandElement.textContent();
104 expect(commandText).toContain('unique-test-command-xyz123');
105 });
106
107 test('think tool shows thought prefix in header', async ({ page }) => {
108 await page.goto('/');
109 await page.waitForLoadState('domcontentloaded');
110
111 const messageInput = page.getByTestId('message-input');
112 const sendButton = page.getByTestId('send-button');
113
114 await messageInput.fill('think: This is a long thought that should be truncated in the header display');
115 await sendButton.click();
116
117 await expect(page.locator('[data-testid="tool-call-completed"]').first()).toBeVisible({ timeout: 30000 });
118
119 // Verify think tool shows truncated thoughts in the header
120 const thinkTool = page.locator('.tool').filter({ hasText: 'This is a long thought' }).first();
121 await expect(thinkTool.locator('.tool-command')).toBeVisible();
122 // The text should be truncated (50 chars max)
123 const headerText = await thinkTool.locator('.tool-command').textContent();
124 expect(headerText?.startsWith('This is a long thought')).toBe(true);
125 });
126
127 test('browser navigate tool shows URL in header', async ({ page }) => {
128 await page.goto('/');
129 await page.waitForLoadState('domcontentloaded');
130
131 const messageInput = page.getByTestId('message-input');
132 const sendButton = page.getByTestId('send-button');
133
134 await messageInput.fill('tool smorgasbord');
135 await sendButton.click();
136
137 await page.waitForFunction(
138 () => document.querySelectorAll('[data-testid="tool-call-completed"]').length >= 9,
139 undefined,
140 { timeout: 30000 }
141 );
142
143 // Verify browser_navigate tool shows URL in the header
144 const navigateTool = page.locator('.tool').filter({ hasText: 'https://example.com' }).first();
145 await expect(navigateTool.locator('.tool-command').filter({ hasText: 'https://example.com' })).toBeVisible();
146 });
147
148 test('patch tool can be collapsed and expanded without errors', async ({ page }) => {
149 await page.goto('/');
150 await page.waitForLoadState('domcontentloaded');
151
152 const messageInput = page.getByTestId('message-input');
153 const sendButton = page.getByTestId('send-button');
154
155 // Trigger a successful patch tool (uses overwrite operation which always succeeds)
156 await messageInput.fill('patch success');
157 await sendButton.click();
158
159 // Wait for successful patch tool with Monaco editor
160 // Use specific locator to find the successful patch (not the failed ones from other tests)
161 const patchTool = page.locator('.patch-tool[data-testid="tool-call-completed"]').filter({ hasText: 'test-patch-success.txt' }).first();
162 await expect(patchTool).toBeVisible({ timeout: 30000 });
163 // Wait for Monaco editor to be fully rendered (only visible for successful patches)
164 await expect(patchTool.locator('.patch-tool-monaco-editor')).toBeVisible({ timeout: 10000 });
165
166 // Get console errors before toggling
167 const errors: string[] = [];
168 page.on('pageerror', (error) => errors.push(error.message));
169
170 const header = patchTool.locator('.patch-tool-header');
171
172 // Collapse
173 await header.click();
174 await expect(patchTool.locator('.patch-tool-details')).toBeHidden();
175
176 // Expand - Monaco should reinitialize
177 await header.click();
178 await expect(patchTool.locator('.patch-tool-details')).toBeVisible();
179 await expect(patchTool.locator('.patch-tool-monaco-editor')).toBeVisible({ timeout: 10000 });
180
181 // Collapse again
182 await header.click();
183 await expect(patchTool.locator('.patch-tool-details')).toBeHidden();
184
185 // Expand again - this was triggering "Cannot add model because it already exists!" in Firefox
186 await header.click();
187 await expect(patchTool.locator('.patch-tool-details')).toBeVisible();
188 await expect(patchTool.locator('.patch-tool-monaco-editor')).toBeVisible({ timeout: 10000 });
189
190 // Check no Monaco model errors occurred
191 const modelErrors = errors.filter(e => e.includes('model') && e.includes('already exists'));
192 expect(modelErrors).toHaveLength(0);
193 });
194
195 test('emoji sizes are consistent across all tools', async ({ page }) => {
196 await page.goto('/');
197 await page.waitForLoadState('domcontentloaded');
198
199 const messageInput = page.getByTestId('message-input');
200 const sendButton = page.getByTestId('send-button');
201
202 await messageInput.fill('tool smorgasbord');
203 await sendButton.click();
204
205 await page.waitForFunction(
206 () => document.querySelectorAll('[data-testid="tool-call-completed"]').length >= 9,
207 undefined,
208 { timeout: 30000 }
209 );
210
211 // Get all tool emojis and check their computed font-size
212 const emojiSizes = await page.$$eval(
213 '.tool-emoji, .bash-tool-emoji, .patch-tool-emoji, .screenshot-tool-emoji',
214 (elements) => elements.map(el => window.getComputedStyle(el).fontSize)
215 );
216
217 // All emojis should be 1rem (16px by default)
218 // Check that all sizes are the same
219 const uniqueSizes = new Set(emojiSizes);
220 expect(uniqueSizes.size).toBe(1);
221
222 // Verify the size is 16px (1rem)
223 expect(emojiSizes[0]).toBe('16px');
224 });
225});