tool-components.spec.ts

  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});