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:
216
tests/helpers/playwright-helpers.ts
Normal file
216
tests/helpers/playwright-helpers.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user