feat: Improve UI layout and navigation

- Increase logo size (48x48 desktop, 56x56 mobile) for better visibility
- Add logo as favicon
- Add logo to mobile header
- Move user menu to navigation bars (sidebar on desktop, bottom bar on mobile)
- Fix desktop chat layout - container structure prevents voice controls cutoff
- Fix mobile bottom bar - use icon-only ActionIcons instead of truncated text buttons
- Hide Create Node/New Conversation buttons on mobile to save header space
- Make fixed header and voice controls work properly with containers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-09 14:43:11 +00:00
parent 0b632a31eb
commit f0284ef813
74 changed files with 6996 additions and 629 deletions

77
tests/helpers/README.md Normal file
View File

@@ -0,0 +1,77 @@
# Playwright Test Helpers
This directory contains reusable helper functions for both Magnitude tests and Playwright MCP testing.
## Usage
### In Magnitude Tests
```typescript
import { test } from 'magnitude-test';
import {
navigateToApp,
loginWithBluesky,
startConversation,
createNodeDraft,
publishNode,
completeNodePublishFlow,
} from '../helpers/playwright-helpers';
test('User can publish a node', async (agent) => {
const page = agent.page; // Get Playwright page from Magnitude agent
await completeNodePublishFlow(page, 'My test conversation');
await agent.check('Node published successfully');
});
```
### In Playwright MCP
Use the helper functions directly with the Playwright MCP page instance:
```typescript
import { loginWithBluesky, startConversation } from './tests/helpers/playwright-helpers';
// In your MCP session
await loginWithBluesky(page);
await startConversation(page, 'Hello AI');
```
## Available Helpers
### Authentication
- `loginWithBluesky(page, credentials?)` - Complete OAuth login flow
- `logout(page)` - Logout from application
- `isLoggedIn(page)` - Check if user is authenticated
### Navigation
- `navigateToApp(page, baseUrl?)` - Go to app home page
### Conversation
- `startConversation(page, message)` - Send first message in chat
- `waitForAIResponse(page, timeout?)` - Wait for AI to finish responding
### Node Publishing
- `createNodeDraft(page)` - Click "Create Node" button
- `editNodeDraft(page, title?, content?)` - Modify draft before publishing
- `publishNode(page)` - Click "Publish" and wait for success
- `completeNodePublishFlow(page, message?, credentials?)` - Full end-to-end flow
## Environment Variables
Set these in your `.env.test` file:
```env
TEST_BLUESKY_USERNAME=your-test-user.bsky.social
TEST_BLUESKY_PASSWORD=your-test-password
```
If not set, helpers will use default values (which will fail for real OAuth).
## Design Principles
1. **Reusability** - Each helper is atomic and can be composed
2. **Reliability** - Helpers wait for network and UI state before proceeding
3. **Flexibility** - Optional parameters allow customization
4. **Error Handling** - Helpers throw clear errors when expectations aren't met

View File

@@ -0,0 +1,216 @@
/**
* Playwright Helper Functions
*
* Reusable test utilities for both Magnitude tests and Playwright MCP.
* These helpers encapsulate common test flows to reduce duplication.
*/
import type { Page } from '@playwright/test';
export interface TestCredentials {
username: string;
password: string;
}
/**
* Default test credentials for Bluesky OAuth login.
* In production tests, these should come from environment variables.
*/
export const DEFAULT_TEST_CREDENTIALS: TestCredentials = {
username: process.env.TEST_BLUESKY_USERNAME || 'test-user.bsky.social',
password: process.env.TEST_BLUESKY_PASSWORD || 'test-password',
};
/**
* Navigate to the application home page and wait for it to load.
*/
export async function navigateToApp(page: Page, baseUrl: string = 'http://localhost:3000'): Promise<void> {
await page.goto(baseUrl);
await page.waitForLoadState('networkidle');
}
/**
* Complete the Bluesky OAuth login flow.
*
* This function:
* 1. Clicks the "Log in with Bluesky" button
* 2. Waits for Bluesky OAuth page to load
* 3. Fills in credentials
* 4. Submits the form
* 5. Waits for redirect back to the app
*
* @param page - Playwright page instance
* @param credentials - Optional custom credentials (defaults to DEFAULT_TEST_CREDENTIALS)
*/
export async function loginWithBluesky(
page: Page,
credentials: TestCredentials = DEFAULT_TEST_CREDENTIALS
): Promise<void> {
// Click login button
await page.click('text="Log in with Bluesky"');
// Wait for Bluesky OAuth page
await page.waitForURL(/bsky\.social|bsky\.app/);
// Fill in credentials
await page.fill('input[name="identifier"]', credentials.username);
await page.fill('input[name="password"]', credentials.password);
// Submit login form
await page.click('button[type="submit"]');
// Wait for redirect back to app
await page.waitForURL(/localhost:3000/);
await page.waitForLoadState('networkidle');
}
/**
* Start a new conversation by sending a message in the chat interface.
*
* @param page - Playwright page instance
* @param message - The message to send
*/
export async function startConversation(page: Page, message: string): Promise<void> {
// Find the chat input (textarea or input field)
const chatInput = page.locator('textarea, input[type="text"]').first();
// Type the message
await chatInput.fill(message);
// Submit (press Enter or click send button)
await chatInput.press('Enter');
// Wait for AI response to appear
await page.waitForSelector('text=/AI|Assistant|thinking/i', { timeout: 10000 });
}
/**
* Create a node draft from the current conversation.
*
* This assumes:
* - User is already in a conversation
* - The "Create Node" button is visible
*/
export async function createNodeDraft(page: Page): Promise<void> {
// Click "Create Node" button
await page.click('text="Create Node"');
// Wait for navigation to edit page
await page.waitForURL(/\/edit/);
await page.waitForLoadState('networkidle');
// Verify we're on the edit page with draft content
await page.waitForSelector('input[value]:not([value=""])', { timeout: 5000 });
}
/**
* Publish a node from the edit page.
*
* This assumes:
* - User is already on the /edit page
* - Draft content is loaded
*/
export async function publishNode(page: Page): Promise<void> {
// Click "Publish Node" button
await page.click('text="Publish Node"');
// Wait for success notification
await page.waitForSelector('text=/Node published|success/i', { timeout: 15000 });
// Wait for navigation back to conversation view
await page.waitForURL(/\/chat|\/conversation/, { timeout: 5000 });
}
/**
* Complete end-to-end flow: Login → Conversation → Create Node → Publish
*
* This is the "happy path" test flow for the core user journey.
*
* @param page - Playwright page instance
* @param message - Message to start conversation with
* @param credentials - Optional custom credentials
*/
export async function completeNodePublishFlow(
page: Page,
message: string = 'Test conversation for node creation',
credentials: TestCredentials = DEFAULT_TEST_CREDENTIALS
): Promise<void> {
await navigateToApp(page);
await loginWithBluesky(page, credentials);
await startConversation(page, message);
await createNodeDraft(page);
await publishNode(page);
}
/**
* Edit node draft content before publishing.
*
* @param page - Playwright page instance
* @param title - New title (undefined to skip)
* @param content - New content (undefined to skip)
*/
export async function editNodeDraft(
page: Page,
title?: string,
content?: string
): Promise<void> {
if (title !== undefined) {
const titleInput = page.locator('input[label="Title"], input[placeholder*="title" i]').first();
await titleInput.fill(title);
}
if (content !== undefined) {
const contentInput = page.locator('textarea[label="Content"], textarea[placeholder*="content" i]').first();
await contentInput.fill(content);
}
}
/**
* Wait for AI to finish responding in the chat.
*
* This looks for the typing indicator to disappear.
*/
export async function waitForAIResponse(page: Page, timeoutMs: number = 30000): Promise<void> {
// Wait for typing indicator to appear
await page.waitForSelector('text=/thinking|typing|generating/i', {
timeout: 5000,
state: 'visible'
}).catch(() => {
// Indicator might appear and disappear quickly, that's okay
});
// Wait for it to disappear
await page.waitForSelector('text=/thinking|typing|generating/i', {
timeout: timeoutMs,
state: 'hidden'
}).catch(() => {
// Might already be gone, that's okay
});
}
/**
* Logout from the application.
*/
export async function logout(page: Page): Promise<void> {
// Click user profile menu
await page.click('[aria-label="User menu"], button:has-text("Profile")');
// Click logout button
await page.click('text="Logout"');
// Wait for redirect to login page
await page.waitForURL(/\/login|\/$/);
}
/**
* Check if user is logged in by looking for authenticated UI elements.
*/
export async function isLoggedIn(page: Page): Promise<boolean> {
try {
// Look for chat interface or user menu
await page.waitForSelector('textarea, [aria-label="User menu"]', { timeout: 2000 });
return true;
} catch {
return false;
}
}