From 86e46fe70510533fa798a3ae88ee14269222c53b Mon Sep 17 00:00:00 2001 From: norman-ma Date: Mon, 22 Dec 2025 14:29:47 +0000 Subject: [PATCH 1/2] feat: Implement system message functionality with styling and tests fix: Resolved currentUserId conflict in tailwind simulator --- README.md | 19 +- src/components/ChatComponent.jsx | 8 +- src/components/ComposeArea.jsx | 46 ++++- src/components/MessageThread.jsx | 18 ++ src/sample-data.js | 2 - src/store.js | 23 +-- tests/README.md | 15 ++ tests/system-messages.spec.js | 309 ++++++++++++++++++++++++++++++ tests/tailwind-simulator.spec.js | 310 +++++++++++++++++++++++++++++++ 9 files changed, 712 insertions(+), 38 deletions(-) create mode 100644 tests/system-messages.spec.js diff --git a/README.md b/README.md index 2eefb56..62282d8 100644 --- a/README.md +++ b/README.md @@ -389,8 +389,8 @@ For detailed examples of receiving and sending messages, see **[EMBEDDING.md](EM ```javascript { type: 'message', - role: 'external' | 'internal', - senderId: 100, // Integer user/patient ID + role: 'external' | 'internal' | 'system', + senderId: 100, // Integer user/patient ID (null for system messages) channel: 'portal' | 'sms' | 'voicemail' | 'auto', time: '2025-10-29 08:12', text: 'Message text' @@ -419,6 +419,21 @@ For detailed examples of receiving and sending messages, see **[EMBEDDING.md](EM - `appt` - Appointments - Additional types can be added as needed +#### System Message + +```javascript +{ + type: 'message', + role: 'system', + senderId: null, + channel: 'auto', + time: '2025-10-29 08:00', + text: 'New conversation initialized.' +} +``` + +System messages are center-aligned in a yellow text box and are used for automated messages like conversation creation notifications. They use `role: 'system'` instead of `'external'` or `'internal'`. + ## Loading Custom Data ```jsx diff --git a/src/components/ChatComponent.jsx b/src/components/ChatComponent.jsx index 079d0c1..b7fc0db 100644 --- a/src/components/ChatComponent.jsx +++ b/src/components/ChatComponent.jsx @@ -34,16 +34,9 @@ const ChatComponent = ({ const setSidebarOpen = useChatStore(state => state.setSidebarOpen); const loadConversations = useChatStore(state => state.loadConversations); const createConversation = useChatStore(state => state.createConversation); - const setCurrentUserId = useChatStore(state => state.setCurrentUserId); - const storedCurrentUserId = useChatStore(state => state.currentUserId); const setActiveConversation = useChatStore(state => state.setActiveConversation); const activeConversation = useChatStore(state => state.getActiveConversation()); - // Sync currentUserId prop with store - React.useEffect(() => { - setCurrentUserId(currentUserId); - }, [currentUserId, setCurrentUserId]); - // Load initial data if provided React.useEffect(() => { if (initialData) { @@ -181,6 +174,7 @@ const ChatComponent = ({ activeConversation={activeConversation} disableClosedConversations={disableClosedConversations} hideDeliveryMethod={hideDeliveryMethod} + onConversationCreated={onConversationCreated} /> diff --git a/src/components/ComposeArea.jsx b/src/components/ComposeArea.jsx index 1b962f9..b005be6 100644 --- a/src/components/ComposeArea.jsx +++ b/src/components/ComposeArea.jsx @@ -6,7 +6,8 @@ const ComposeArea = ({ currentUserId = null, activeConversation, disableClosedConversations = false, - hideDeliveryMethod = false + hideDeliveryMethod = false, + onConversationCreated = null }) => { const [text, setText] = useState(''); const [sendType, setSendType] = useState('auto'); @@ -15,6 +16,9 @@ const ComposeArea = ({ const fileInputRef = useRef(null); const addMessage = useChatStore(state => state.addMessage); + const conversations = useChatStore(state => state.conversations); + const createConversation = useChatStore(state => state.createConversation); + const getActiveConversation = useChatStore(state => state.getActiveConversation); // Fix: Use 'open' property instead of 'status', and add null check const isClosed = disableClosedConversations && activeConversation && !activeConversation.open; @@ -133,7 +137,20 @@ const ComposeArea = ({ }; const handleSend = () => { - if ((!text.trim() && images.length === 0) || isClosed || !activeConversation) return; + if ((!text.trim() && images.length === 0) || isClosed ) return; + + // If no conversations exist, create a new one + let conversationToUse = activeConversation; + let newlyCreatedConversation = false; + + if (!activeConversation && conversations.length === 0) { + const newConversation = createConversation('New Conversation'); + conversationToUse = newConversation; + newlyCreatedConversation = true; + } + + // If still no conversation (edge case), return + if (!conversationToUse) return; const message = { text: text.trim(), @@ -144,12 +161,25 @@ const ComposeArea = ({ const newMessage = addMessage(message); - // Call the callback if provided - if (onMessageSent && newMessage) { - onMessageSent({ - conversationId: activeConversation.id, - message: newMessage, - }); + // If we created a new conversation, trigger onConversationCreated with full conversation including the message + if (newlyCreatedConversation) { + if (onConversationCreated) { + const updatedConversation = getActiveConversation(); + if (updatedConversation) { + onConversationCreated({ + conversationId: updatedConversation.id, + conversation: updatedConversation + }); + } + } + } else { + // Only call onMessageSent if we didn't just create a conversation + if (onMessageSent && newMessage) { + onMessageSent({ + conversationId: conversationToUse.id, + message: newMessage, + }); + } } setText(''); diff --git a/src/components/MessageThread.jsx b/src/components/MessageThread.jsx index 706d34d..90c7593 100644 --- a/src/components/MessageThread.jsx +++ b/src/components/MessageThread.jsx @@ -8,6 +8,24 @@ const MessageItem = ({ item, currentUserId, linkBuilder }) => { }); if (item.type === 'message') { + // System messages are center-aligned + if (item.role === 'system') { + return ( +
+
+
+ ') }} /> +
+
+ {time} +
+
+
+ ); + } + // Message is on the right if current user sent it const isCurrentUser = item.senderId === currentUserId; const senderLabel = item.sender_name || (isCurrentUser ? 'You' : 'Other'); diff --git a/src/sample-data.js b/src/sample-data.js index adac71d..ffaec20 100644 --- a/src/sample-data.js +++ b/src/sample-data.js @@ -62,13 +62,11 @@ export const sampleConversations = [ ]; export const sampleActiveConversationId = 735; -export const sampleCurrentUserId = null; // Helper function to get initial demo state export function getDemoInitialState() { return { conversations: sampleConversations, activeConversationId: sampleActiveConversationId, - currentUserId: sampleCurrentUserId, }; } diff --git a/src/store.js b/src/store.js index 5462ed4..acdd3af 100644 --- a/src/store.js +++ b/src/store.js @@ -25,7 +25,6 @@ const useChatStore = create((set, get) => ({ activeConversationId: null, searchQuery: '', sidebarOpen: false, - currentUserId: null, conversationsLoading: false, conversationsHasMore: false, threadLoadingStates: {}, // { [conversationId]: { isLoading: boolean, hasMore: boolean } } @@ -131,15 +130,12 @@ const useChatStore = create((set, get) => ({ lastActivity: now, thread: [ { - type: 'ref', - refType: 'appt', - refId: null, - title: 'Conversation Created', - role: 'internal', + type: 'message', + role: 'system', senderId: null, channel: 'auto', time: now, - text: 'Conversation Created: New conversation initialized.' + text: 'New conversation initialized.' } ] }; @@ -166,12 +162,6 @@ const useChatStore = create((set, get) => ({ set({ sidebarOpen: open }); }, - // Set current user ID - setCurrentUserId: (userId) => { - const intUserId = userId !== null && userId !== undefined ? parseInt(userId, 10) : null; - set({ currentUserId: intUserId }); - }, - // Replace entire state with new data loadConversations: (data) => { // Ensure all IDs are integers @@ -192,14 +182,10 @@ const useChatStore = create((set, get) => ({ const activeId = data.activeConversationId ? parseInt(data.activeConversationId, 10) : conversations[0]?.id || null; - const userId = data.currentUserId !== undefined - ? parseInt(data.currentUserId, 10) - : null; set({ conversations, activeConversationId: activeId, - ...(userId !== null && { currentUserId: userId }), }); }, @@ -320,11 +306,10 @@ const useChatStore = create((set, get) => ({ // Export current state exportState: () => { - const { conversations, activeConversationId, currentUserId } = get(); + const { conversations, activeConversationId } = get(); return { conversations, activeConversationId, - currentUserId, }; }, })); diff --git a/tests/README.md b/tests/README.md index 02b4543..e9ec89a 100644 --- a/tests/README.md +++ b/tests/README.md @@ -40,6 +40,21 @@ Tests the Tailwind demo simulator functionality: - Responsive layout in simulator mode - Rapid toggling stability +### `system-messages.spec.js` +Tests system message functionality: +- System message creation when new conversation is created +- Center alignment of system messages +- Yellow background styling (distinct from regular messages) +- No sender name displayed for system messages +- Timestamp display +- Visual distinction from regular messages +- Proper rendering with text-center alignment +- Handling multiple system messages in a conversation +- No channel icons displayed for system messages + +### `link-builder.spec.js` +Tests custom link builder functionality for references. + ## Running Tests ### Run all tests diff --git a/tests/system-messages.spec.js b/tests/system-messages.spec.js new file mode 100644 index 0000000..cbf4176 --- /dev/null +++ b/tests/system-messages.spec.js @@ -0,0 +1,309 @@ +import { test, expect } from '@playwright/test'; + +test.describe('System Messages', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/demo-tailwind.html'); + // Wait for React component to mount + await page.waitForSelector('[data-testid="chat-component"], .chat-component, [class*="chat"]', { timeout: 10000 }); + }); + + test('should display system message when creating new conversation', async ({ page }) => { + // Click New Conversation button + const newButton = page.locator('button[aria-label*="new conversation" i]'); + await newButton.click(); + + // Wait for modal/dialog + await page.waitForTimeout(300); + + // Enter conversation title and create + const titleInput = page.locator('input[type="text"]').first(); + await titleInput.fill('Test System Message'); + + const createButton = page.locator('button:has-text("Create")'); + await createButton.click(); + + // Wait for new conversation to be created + await page.waitForTimeout(500); + + // System message should be visible + const systemMessage = page.locator('text=/New conversation initialized/i'); + await expect(systemMessage).toBeVisible(); + }); + + test('should center-align system messages', async ({ page }) => { + // Create a new conversation to get a system message + const newButton = page.locator('button[aria-label*="new conversation" i]'); + await newButton.click(); + await page.waitForTimeout(300); + + const titleInput = page.locator('input[type="text"]').first(); + await titleInput.fill('Center Align Test'); + + const createButton = page.locator('button:has-text("Create")'); + await createButton.click(); + await page.waitForTimeout(500); + + // Find the system message - it should be visible + const systemMessage = page.locator('text=/New conversation initialized/i'); + await expect(systemMessage).toBeVisible(); + + // Get the message box position and verify it's centered + const messageBox = systemMessage.locator('../..'); + const boundingBox = await messageBox.boundingBox(); + const viewportSize = page.viewportSize(); + + // System message should be roughly centered (within middle third of screen) + if (boundingBox && viewportSize) { + const centerX = boundingBox.x + boundingBox.width / 2; + const screenCenterX = viewportSize.width / 2; + const distanceFromCenter = Math.abs(centerX - screenCenterX); + + // Should be within 30% of screen width from center + expect(distanceFromCenter).toBeLessThan(viewportSize.width * 0.3); + } + }); + + test('should style system messages with yellow background', async ({ page }) => { + // Create a new conversation + const newButton = page.locator('button[aria-label*="new conversation" i]'); + await newButton.click(); + await page.waitForTimeout(300); + + const titleInput = page.locator('input[type="text"]').first(); + await titleInput.fill('Yellow Background Test'); + + const createButton = page.locator('button:has-text("Create")'); + await createButton.click(); + await page.waitForTimeout(500); + + // System message should be visible + const systemMessage = page.locator('text=/New conversation initialized/i'); + await expect(systemMessage).toBeVisible(); + + // Take screenshot to verify styling (visual verification) + await page.screenshot({ path: 'test-results/system-message-yellow-bg.png' }); + }); + + test('should not show sender name for system messages', async ({ page }) => { + // Create a new conversation + const newButton = page.locator('button[aria-label*="new conversation" i]'); + await newButton.click(); + await page.waitForTimeout(300); + + const titleInput = page.locator('input[type="text"]').first(); + await titleInput.fill('No Sender Test'); + + const createButton = page.locator('button:has-text("Create")'); + await createButton.click(); + await page.waitForTimeout(500); + + // Find the system message container + const systemMessageContainer = page.locator('text=/New conversation initialized/i').locator('../..'); + + // System messages should not have sender_name displayed + // Check that there's no sender name element near the system message + const hasSenderName = await systemMessageContainer.locator('text=/Dr\\.|Jane|Sender/i').count(); + expect(hasSenderName).toBe(0); + }); + + test('should display timestamp for system messages', async ({ page }) => { + // Create a new conversation + const newButton = page.locator('button[aria-label*="new conversation" i]'); + await newButton.click(); + await page.waitForTimeout(300); + + const titleInput = page.locator('input[type="text"]').first(); + await titleInput.fill('Timestamp Test'); + + const createButton = page.locator('button:has-text("Create")'); + await createButton.click(); + await page.waitForTimeout(500); + + // System message should be visible + const systemMessage = page.locator('text=/New conversation initialized/i'); + await expect(systemMessage).toBeVisible(); + + // System message container should have text content (message + timestamp) + const messageContainer = systemMessage.locator('../..'); + const textContent = await messageContainer.textContent(); + expect(textContent).toBeTruthy(); + expect(textContent.length).toBeGreaterThan(20); // Should have message text plus timestamp + }); + + test('should distinguish system messages from regular messages visually', async ({ page }) => { + // Create a new conversation + const newButton = page.locator('button[aria-label*="new conversation" i]'); + await newButton.click(); + await page.waitForTimeout(300); + + const titleInput = page.locator('input[type="text"]').first(); + await titleInput.fill('Visual Distinction Test'); + + const createButton = page.locator('button:has-text("Create")'); + await createButton.click(); + await page.waitForTimeout(500); + + // Send a regular message + const textarea = page.locator('textarea').first(); + await textarea.fill('This is a regular message'); + await textarea.press('Enter'); + await page.waitForTimeout(500); + + // Get system message background color + const systemMessageBox = page.locator('text=/New conversation initialized/i').locator('..'); + const systemBgColor = await systemMessageBox.evaluate(el => + window.getComputedStyle(el).backgroundColor + ); + + // Get regular message background color + const regularMessageBox = page.locator('text=/This is a regular message/i').locator('..'); + const regularBgColor = await regularMessageBox.evaluate(el => + window.getComputedStyle(el).backgroundColor + ); + + // They should have different background colors + expect(systemBgColor).not.toBe(regularBgColor); + }); + + test('should render system messages with proper styling', async ({ page }) => { + // Create a new conversation + const newButton = page.locator('button[aria-label*="new conversation" i]'); + await newButton.click(); + await page.waitForTimeout(300); + + const titleInput = page.locator('input[type="text"]').first(); + await titleInput.fill('Styling Test'); + + const createButton = page.locator('button:has-text("Create")'); + await createButton.click(); + await page.waitForTimeout(500); + + // System message should be visible and distinct + const systemMessage = page.locator('text=/New conversation initialized/i'); + await expect(systemMessage).toBeVisible(); + + // Verify message is displayed (basic functional test) + const textContent = await systemMessage.textContent(); + expect(textContent).toContain('New conversation initialized'); + }); + + test('should handle multiple system messages in conversation', async ({ page }) => { + // Navigate and wait for the component + await page.waitForTimeout(500); + + // Use the exposed store API to add test data + const testDataLoaded = await page.evaluate(() => { + try { + // Access the global useChatStore that should be exposed + if (typeof window.useChatStore === 'undefined') { + return false; + } + + const now = new Date().toISOString().slice(0, 16).replace('T', ' '); + const testConversation = { + id: 9999, + title: 'Multiple System Messages', + reference_id: null, + open: true, + unread: false, + lastActivity: now, + thread: [ + { + type: 'message', + role: 'system', + senderId: null, + channel: 'auto', + time: now, + text: 'First system message' + }, + { + type: 'message', + role: 'external', + senderId: 100, + sender_name: 'Jane Doe', + channel: 'portal', + time: now, + text: 'Regular message' + }, + { + type: 'message', + role: 'system', + senderId: null, + channel: 'auto', + time: now, + text: 'Second system message' + } + ] + }; + + window.useChatStore.getState().loadConversations({ + conversations: [testConversation], + activeConversationId: 9999 + }); + return true; + } catch (e) { + console.error('Failed to load test data:', e); + return false; + } + }); + + if (testDataLoaded) { + await page.waitForTimeout(500); + + // Both system messages should be visible + const firstSystemMsg = page.locator('text=/First system message/i'); + const secondSystemMsg = page.locator('text=/Second system message/i'); + + await expect(firstSystemMsg).toBeVisible(); + await expect(secondSystemMsg).toBeVisible(); + } else { + // Skip this test if we can't inject data - just create two conversations instead + const newButton = page.locator('button[aria-label*="new conversation" i]'); + + // Create first conversation + await newButton.click(); + await page.waitForTimeout(300); + const titleInput = page.locator('input[type="text"]').first(); + await titleInput.fill('First Test'); + const createButton = page.locator('button:has-text("Create")'); + await createButton.click(); + await page.waitForTimeout(500); + + // Verify system message appears + const systemMessage = page.locator('text=/New conversation initialized/i').first(); + await expect(systemMessage).toBeVisible(); + } + }); + + test('system messages should not have channel icons', async ({ page }) => { + // Create a new conversation + const newButton = page.locator('button[aria-label*="new conversation" i]'); + await newButton.click(); + await page.waitForTimeout(300); + + const titleInput = page.locator('input[type="text"]').first(); + await titleInput.fill('No Channel Icon Test'); + + const createButton = page.locator('button:has-text("Create")'); + await createButton.click(); + await page.waitForTimeout(500); + + // Send a regular message to compare + const textarea = page.locator('textarea').first(); + await textarea.fill('Regular message with icon'); + await textarea.press('Enter'); + await page.waitForTimeout(500); + + // Regular messages have channel icons (emoji like 📱, 💻, etc.) + const regularMessageBox = page.locator('text=/Regular message with icon/i').locator('..'); + const hasIcon = await regularMessageBox.locator('span').count() > 0; + + // System message should not have channel icon in the same way + const systemMessageBox = page.locator('text=/New conversation initialized/i').locator('..'); + const systemText = await systemMessageBox.textContent(); + + // System message should only contain text and timestamp, no channel labels + expect(systemText).not.toContain('Portal'); + expect(systemText).not.toContain('SMS'); + }); +}); diff --git a/tests/tailwind-simulator.spec.js b/tests/tailwind-simulator.spec.js index 21f7c6a..c0da7c4 100644 --- a/tests/tailwind-simulator.spec.js +++ b/tests/tailwind-simulator.spec.js @@ -240,6 +240,316 @@ test.describe('Tailwind Demo - Simulator Functionality', () => { }); }); +test.describe('Tailwind Demo - Multi-Component User ID Independence', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/demo-tailwind.html'); + await page.waitForSelector('.chat-component-root', { timeout: 10000 }); + }); + + test('should maintain separate currentUserId for each component in simulator mode', async ({ page }) => { + // Enable simulator mode + const simulatorButton = page.locator('button:has-text("Enable Simulator")'); + await simulatorButton.click(); + await page.waitForTimeout(500); + + // Verify two components exist + const chatComponents = page.locator('.chat-component-root'); + await expect(chatComponents).toHaveCount(2); + + // Verify component labels indicate different users + await expect(page.locator('text=Component 1 - Internal User (Clinician)')).toBeVisible(); + await expect(page.locator('text=Component 2 - External User (Patient)')).toBeVisible(); + }); + + test('should show shared messages in both components', async ({ page }) => { + // Enable simulator mode + const simulatorButton = page.locator('button:has-text("Enable Simulator")'); + await simulatorButton.click(); + await page.waitForTimeout(500); + + const chatComponents = page.locator('.chat-component-root'); + await expect(chatComponents).toHaveCount(2); + + // Look for sample message text that exists in the demo data + // Both components should show the same messages since they share state + const message1 = page.locator('text=/Good morning.*pain in my right side/i'); + const message2 = page.locator('text=/bloodwork shows a mild infection/i'); + + // These sample messages should be visible (might appear in multiple places due to two components) + expect(await message1.count()).toBeGreaterThan(0); + expect(await message2.count()).toBeGreaterThan(0); + }); + + test('should send messages with correct sender alignment in each component', async ({ page }) => { + // Enable simulator mode + const simulatorButton = page.locator('button:has-text("Enable Simulator")'); + await simulatorButton.click(); + await page.waitForTimeout(500); + + const chatComponents = page.locator('.chat-component-root'); + + // Component 1 (Internal User, ID 200) - their messages should align right + const component1 = chatComponents.nth(0); + const textarea1 = component1.locator('textarea').first(); + const sendButton1 = component1.locator('button[aria-label="Send message"]').first(); + + if (await textarea1.isVisible() && await sendButton1.isVisible()) { + const testMessage1 = 'Test from Internal User 200'; + await textarea1.fill(testMessage1); + await sendButton1.click(); + await page.waitForTimeout(1000); + + // Message should appear in both components + const messageInComponent1 = component1.locator(`text="${testMessage1}"`); + const messageInComponent2 = chatComponents.nth(1).locator(`text="${testMessage1}"`); + + await expect(messageInComponent1).toBeVisible(); + await expect(messageInComponent2).toBeVisible(); + + // In component 1 (sender is currentUser), message should be on the right + const message1Parent = messageInComponent1.locator('xpath=ancestor::div[contains(@class, "tw-items-end")]').first(); + if (await message1Parent.count() > 0) { + // Message is right-aligned (items-end) in component 1 + expect(true).toBeTruthy(); + } + } + }); + + test('should send messages from component 2 with external user alignment', async ({ page }) => { + // Enable simulator mode + const simulatorButton = page.locator('button:has-text("Enable Simulator")'); + await simulatorButton.click(); + await page.waitForTimeout(500); + + const chatComponents = page.locator('.chat-component-root'); + + // Component 2 (External User, ID 100) - their messages should align right in component 2 + const component2 = chatComponents.nth(1); + const textarea2 = component2.locator('textarea').first(); + const sendButton2 = component2.locator('button[aria-label="Send message"]').first(); + + if (await textarea2.isVisible() && await sendButton2.isVisible()) { + const testMessage2 = 'Test from External User 100'; + await textarea2.fill(testMessage2); + await sendButton2.click(); + await page.waitForTimeout(1000); + + // Message should appear in both components + const messageInComponent1 = chatComponents.nth(0).locator(`text="${testMessage2}"`); + const messageInComponent2 = component2.locator(`text="${testMessage2}"`); + + await expect(messageInComponent1).toBeVisible(); + await expect(messageInComponent2).toBeVisible(); + + // In component 2 (sender is currentUser), message should be on the right + const message2Parent = messageInComponent2.locator('xpath=ancestor::div[contains(@class, "tw-items-end")]').first(); + if (await message2Parent.count() > 0) { + // Message is right-aligned (items-end) in component 2 + expect(true).toBeTruthy(); + } + } + }); + + test('should not have currentUserId in shared store state', async ({ page }) => { + // Enable simulator mode + const simulatorButton = page.locator('button:has-text("Enable Simulator")'); + await simulatorButton.click(); + await page.waitForTimeout(500); + + // Check the Zustand store state via the exportState function + const storeState = await page.evaluate(() => { + // Access the store through window if exposed, or via React DevTools + // This assumes useChatStore is accessible + return window.useChatStoreForTest?.getState?.() || null; + }); + + // If we can access the state, verify currentUserId is not present + if (storeState !== null) { + expect(storeState).not.toHaveProperty('currentUserId'); + } + }); + + test('should maintain independent user context after toggling conversations', async ({ page }) => { + // Enable simulator mode + const simulatorButton = page.locator('button:has-text("Enable Simulator")'); + await simulatorButton.click(); + await page.waitForTimeout(500); + + const chatComponents = page.locator('.chat-component-root'); + + // Click on a conversation in component 1 + const component1 = chatComponents.nth(0); + const conversationListItem = component1.locator('[role="button"]').first(); + + if (await conversationListItem.isVisible()) { + await conversationListItem.click(); + await page.waitForTimeout(500); + + // Verify both components still show correct user labels + await expect(page.locator('text=Component 1 - Internal User (Clinician)')).toBeVisible(); + await expect(page.locator('text=Component 2 - External User (Patient)')).toBeVisible(); + } + }); + + test('should preserve currentUserId when switching between simulator and single mode', async ({ page }) => { + // Start with single component + let chatComponents = page.locator('.chat-component-root'); + await expect(chatComponents).toHaveCount(1); + + // Send a message from component 1 + const textarea = page.locator('textarea').first(); + const sendButton = page.locator('button[aria-label="Send message"]').first(); + + if (await textarea.isVisible() && await sendButton.isVisible()) { + await textarea.fill('Message before simulator'); + await sendButton.click(); + await page.waitForTimeout(1000); + } + + // Enable simulator mode + const simulatorButton = page.locator('button:has-text("Enable Simulator")'); + await simulatorButton.click(); + await page.waitForTimeout(500); + + // Should have 2 components now + chatComponents = page.locator('.chat-component-root'); + await expect(chatComponents).toHaveCount(2); + + // Send a message from component 2 + const component2 = chatComponents.nth(1); + const textarea2 = component2.locator('textarea').first(); + const sendButton2 = component2.locator('button[aria-label="Send message"]').first(); + + if (await textarea2.isVisible() && await sendButton2.isVisible()) { + await textarea2.fill('Message from simulator component 2'); + await sendButton2.click(); + await page.waitForTimeout(1000); + } + + // Disable simulator mode + await page.locator('button:has-text("✓ Simulator Active")').click(); + await page.waitForTimeout(500); + + // Should have 1 component again + chatComponents = page.locator('.chat-component-root'); + await expect(chatComponents).toHaveCount(1); + + // All messages should still be visible + await expect(page.locator('text=Message before simulator')).toBeVisible(); + await expect(page.locator('text=Message from simulator component 2')).toBeVisible(); + }); + + test('should handle rapid component creation without user ID conflicts', async ({ page }) => { + // Rapidly toggle simulator mode multiple times + const simulatorButton = page.locator('button:has-text("Enable Simulator"), button:has-text("✓ Simulator Active")'); + + for (let i = 0; i < 3; i++) { + await simulatorButton.click(); + await page.waitForTimeout(200); + } + + // Should end in a stable state + const chatComponents = page.locator('.chat-component-root'); + const componentCount = await chatComponents.count(); + + // Should have either 1 or 2 components depending on final state + expect(componentCount).toBeGreaterThanOrEqual(1); + expect(componentCount).toBeLessThanOrEqual(2); + }); + + test('should export state without currentUserId property', async ({ page }) => { + // Click the Export State button + const exportButton = page.locator('button:has-text("Export State")'); + + // Listen for console logs + const consoleLogs = []; + page.on('console', msg => { + if (msg.type() === 'log' && msg.text().includes('Exported state:')) { + consoleLogs.push(msg.text()); + } + }); + + await exportButton.click(); + await page.waitForTimeout(1000); + + // Check the alert text + page.once('dialog', async dialog => { + expect(dialog.message()).toContain('State exported to console'); + await dialog.accept(); + }); + + // Verify the exported state structure via page evaluation + const exportedState = await page.evaluate(() => { + // Get the store's export function + if (window.useChatStoreForTest) { + return window.useChatStoreForTest.getState().exportState(); + } + return null; + }); + + if (exportedState !== null) { + expect(exportedState).toHaveProperty('conversations'); + expect(exportedState).toHaveProperty('activeConversationId'); + expect(exportedState).not.toHaveProperty('currentUserId'); + } + }); +}); + +test.describe('Tailwind Demo - Multi-Component with Read-Only Mode', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/demo-tailwind.html'); + await page.waitForSelector('.chat-component-root', { timeout: 10000 }); + }); + + test('should maintain currentUserId independence with read-only component', async ({ page }) => { + // Enable simulator mode + const simulatorButton = page.locator('button:has-text("Enable Simulator")'); + await simulatorButton.click(); + await page.waitForTimeout(500); + + // Enable read-only mode + const readOnlyButton = page.locator('button:has-text("Show Read-Only Mode")'); + await readOnlyButton.click(); + await page.waitForTimeout(500); + + // Should now have 3 components: 1 read-only + 2 simulator + const chatComponents = page.locator('.chat-component-root'); + await expect(chatComponents).toHaveCount(3); + + // Verify read-only component label + await expect(page.locator('text=📖 Read-Only Conversation View')).toBeVisible(); + + // Verify simulator components still maintain their labels + await expect(page.locator('text=Component 1 - Internal User (Clinician)')).toBeVisible(); + await expect(page.locator('text=Component 2 - External User (Patient)')).toBeVisible(); + }); + + test('should handle read-only component with different currentUserId', async ({ page }) => { + // Enable read-only mode first + const readOnlyButton = page.locator('button:has-text("Show Read-Only Mode")'); + await readOnlyButton.click(); + await page.waitForTimeout(500); + + // Should have 2 components: 1 read-only + 1 normal + let chatComponents = page.locator('.chat-component-root'); + await expect(chatComponents).toHaveCount(2); + + // Enable simulator mode + const simulatorButton = page.locator('button:has-text("Enable Simulator")'); + await simulatorButton.click(); + await page.waitForTimeout(500); + + // Should now have 3 components + chatComponents = page.locator('.chat-component-root'); + await expect(chatComponents).toHaveCount(3); + + // All components should be functional + await expect(page.locator('text=📖 Read-Only Conversation View')).toBeVisible(); + await expect(page.locator('text=Component 1 - Internal User (Clinician)')).toBeVisible(); + await expect(page.locator('text=Component 2 - External User (Patient)')).toBeVisible(); + }); +}); + test.describe('Tailwind Demo - Simulator Message Alignment', () => { test.beforeEach(async ({ page }) => { await page.goto('/demo-tailwind.html'); From e5845a856171423aecca0fdf1df09adf2f03000c Mon Sep 17 00:00:00 2001 From: norman-ma Date: Mon, 22 Dec 2025 17:41:25 +0000 Subject: [PATCH 2/2] fix: Update temporary test image file names for clarity in image attachment tests --- tests/image-attachment.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/image-attachment.spec.js b/tests/image-attachment.spec.js index 3efdbf7..a3c5e2b 100644 --- a/tests/image-attachment.spec.js +++ b/tests/image-attachment.spec.js @@ -45,7 +45,7 @@ test.describe('Image Attachment Feature', () => { const attachButton = page.getByRole('button', { name: 'Attach image' }); // Create a temporary test image file - const testImagePath = path.join(process.cwd(), 'test-image.png'); + const testImagePath = path.join(process.cwd(), 'test-image-preview.png'); fs.writeFileSync(testImagePath, createTestImageBuffer()); try { @@ -75,7 +75,7 @@ test.describe('Image Attachment Feature', () => { const attachButton = page.getByRole('button', { name: 'Attach image' }); // Create a temporary test image file - const testImagePath = path.join(process.cwd(), 'test-image.png'); + const testImagePath = path.join(process.cwd(), 'test-image-remove.png'); fs.writeFileSync(testImagePath, createTestImageBuffer()); try { @@ -109,7 +109,7 @@ test.describe('Image Attachment Feature', () => { const attachButton = page.getByRole('button', { name: 'Attach image' }); // Create a temporary test image file - const testImagePath = path.join(process.cwd(), 'test-image.png'); + const testImagePath = path.join(process.cwd(), 'test-image-send.png'); fs.writeFileSync(testImagePath, createTestImageBuffer()); try { @@ -158,7 +158,7 @@ test.describe('Image Attachment Feature', () => { const attachButton = page.getByRole('button', { name: 'Attach image' }); // Create a temporary test image file - const testImagePath = path.join(process.cwd(), 'test-image.png'); + const testImagePath = path.join(process.cwd(), 'test-image-only.png'); fs.writeFileSync(testImagePath, createTestImageBuffer()); try {