204 lines
8.6 KiB
TypeScript
204 lines
8.6 KiB
TypeScript
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');
|
|
});
|
|
});
|