import { test, expect } from '@playwright/test'; test.describe('Claude Permissions Workflow', () => { 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'); }); });