import { test, expect } from '@playwright/test'; test.describe('Claude Permissions Workflow', () => { test('vibrates on mobile when permission request is received', async ({ page }) => { // Increase timeout for this test since it involves real Claude interaction test.setTimeout(180000); // Mock navigator.vibrate and track calls await page.addInitScript(() => { (window as unknown as { vibrateCalls: number[][] }).vibrateCalls = []; navigator.vibrate = (pattern: VibratePattern) => { const patternArray = Array.isArray(pattern) ? pattern : [pattern]; (window as unknown as { vibrateCalls: number[][] }).vibrateCalls.push(patternArray); return true; }; }); // Enable console logging for debugging page.on('console', (msg) => { console.log(`[Browser ${msg.type()}]`, msg.text()); }); // 1. Navigate to homepage await page.goto('/'); await expect(page).toHaveTitle(/Spiceflow/i); // 2. Click the + button to open new session menu const createButton = page.locator('button[title="New Session"]'); await expect(createButton).toBeVisible(); await createButton.click(); // 3. Select Claude Code from the dropdown const claudeOption = page.locator('button:has-text("Claude Code")'); await expect(claudeOption).toBeVisible(); await claudeOption.click(); // 4. Wait for navigation to session page await page.waitForURL(/\/session\/.+/); console.log('[Test] Navigated to session page:', page.url()); // 5. Wait for the page to load await expect(page.locator('text=Loading')).not.toBeVisible({ timeout: 5000 }); await expect(page.locator('text=No messages yet')).toBeVisible(); // 6. Send message asking Claude to create a file (which will trigger permission) const textarea = page.locator('textarea'); await expect(textarea).toBeVisible(); await textarea.fill( 'Create a file called vibrate-test.md containing just "test". No other commentary.' ); const sendButton = page.locator('button[type="submit"]'); await expect(sendButton).toBeEnabled(); await sendButton.click(); // 7. Wait for permission request UI to appear const permissionUI = page.locator('text=Claude needs permission'); await expect(permissionUI).toBeVisible({ timeout: 60000 }); console.log('[Test] Permission request UI appeared'); // 8. Check that vibrate was called with the expected pattern const vibrateCalls = await page.evaluate(() => { return (window as unknown as { vibrateCalls: number[][] }).vibrateCalls; }); console.log('[Test] Vibrate calls:', vibrateCalls); expect(vibrateCalls.length).toBeGreaterThan(0); expect(vibrateCalls[0]).toEqual([200, 100, 200]); console.log('[Test] Vibration triggered correctly with pattern [200, 100, 200]'); // Cleanup: Accept and let Claude finish const acceptButton = page.locator('button:has-text("Accept")'); await acceptButton.click(); await expect(permissionUI).not.toBeVisible({ timeout: 10000 }); }); test('permission approval allows file creation and reading', async ({ page }) => { // Increase timeout for this test since it involves real Claude interaction test.setTimeout(180000); // Enable console logging for debugging page.on('console', (msg) => { console.log(`[Browser ${msg.type()}]`, msg.text()); }); // Log WebSocket frames for debugging page.on('websocket', (ws) => { console.log(`[WebSocket] Connected to ${ws.url()}`); ws.on('framesent', (frame) => console.log(`[WS Sent]`, frame.payload)); ws.on('framereceived', (frame) => console.log(`[WS Received]`, frame.payload)); }); // 1. Navigate to homepage await page.goto('/'); await expect(page).toHaveTitle(/Spiceflow/i); // 2. Click the + button to open new session menu const createButton = page.locator('button[title="New Session"]'); await expect(createButton).toBeVisible(); await createButton.click(); // 3. Select Claude Code from the dropdown const claudeOption = page.locator('button:has-text("Claude Code")'); await expect(claudeOption).toBeVisible(); await claudeOption.click(); // 4. Wait for navigation to session page await page.waitForURL(/\/session\/.+/); console.log('[Test] Navigated to session page:', page.url()); // 5. Wait for the page to load await expect(page.locator('text=Loading')).not.toBeVisible({ timeout: 5000 }); await expect(page.locator('text=No messages yet')).toBeVisible(); // 6. Send message asking Claude to create foo.md with a haiku const textarea = page.locator('textarea'); await expect(textarea).toBeVisible(); await textarea.fill( 'Create a file called foo.md containing a haiku about software testing. Title it "My Haiku" at the top. Just create the file, no other commentary needed.' ); const sendButton = page.locator('button[type="submit"]'); await expect(sendButton).toBeEnabled(); await sendButton.click(); // 7. Verify user message appears await expect( page.locator('text=Create a file called foo.md') ).toBeVisible(); console.log('[Test] User message displayed'); // 7b. Verify thinking indicator appears immediately // The thinking indicator shows bouncing dots const bouncingDotsIndicator = page.locator('.animate-bounce'); await expect(bouncingDotsIndicator.first()).toBeVisible({ timeout: 2000 }); console.log('[Test] Thinking indicator appeared immediately'); // 8. Wait for permission request UI to appear const permissionUI = page.locator('text=Claude needs permission'); await expect(permissionUI).toBeVisible({ timeout: 60000 }); console.log('[Test] Permission request UI appeared'); // 9. Verify the permission shows Write tool for foo.md // The permission UI has li > button.font-mono or li > div.font-mono with "Write:" and filename const permissionDescription = page.locator('.font-mono').filter({ hasText: /Write.*foo\.md|create.*foo\.md/i }).first(); await expect(permissionDescription).toBeVisible(); console.log('[Test] Permission shows foo.md file creation'); // 10. Click Accept button const acceptButton = page.locator('button:has-text("Accept")'); await expect(acceptButton).toBeVisible(); await acceptButton.click(); console.log('[Test] Clicked Accept button'); // 11. Wait for permission UI to disappear await expect(permissionUI).not.toBeVisible({ timeout: 10000 }); console.log('[Test] Permission UI disappeared'); // 12. Wait for streaming to complete after permission granted await page.waitForTimeout(2000); const bouncingDots = page.locator('.animate-bounce'); // Only look for pulsing cursor inside markdown-content (not the header status indicator) const pulsingCursor = page.locator('.markdown-content .animate-pulse'); await expect(bouncingDots).toHaveCount(0, { timeout: 60000 }); await expect(pulsingCursor).toHaveCount(0, { timeout: 60000 }); console.log('[Test] First streaming complete'); // Count current messages with markdown-content before sending new request // Assistant messages have .markdown-content inside the bubble const assistantMessages = page.locator('.rounded-lg.border').filter({ has: page.locator('.markdown-content') }); const messageCountBefore = await assistantMessages.count(); console.log('[Test] Message count before read request:', messageCountBefore); // 13. Now ask Claude to read the file back to verify it was created await textarea.fill('Read the contents of foo.md and tell me what it says. Quote the file contents.'); await sendButton.click(); console.log('[Test] Asked Claude to read the file'); // 14. Wait for a NEW message to appear await expect(assistantMessages).toHaveCount(messageCountBefore + 1, { timeout: 60000 }); console.log('[Test] New message appeared'); // Wait for streaming to complete on the new message await expect(bouncingDots).toHaveCount(0, { timeout: 60000 }); await expect(pulsingCursor).toHaveCount(0, { timeout: 60000 }); console.log('[Test] Second streaming complete'); // 15. Verify the response contains "My Haiku" - confirming file was created and read const lastAssistantMessage = assistantMessages.last(); const responseText = await lastAssistantMessage.locator('.markdown-content').textContent(); console.log('[Test] Claude read back:', responseText); // The response should contain "My Haiku" which we asked Claude to title the file expect(responseText).toBeTruthy(); expect(responseText).toContain('My Haiku'); console.log('[Test] Successfully verified "My Haiku" in Claude response'); }); });