diff --git a/ai_animation/package-lock.json b/ai_animation/package-lock.json index 4d673f6..d705516 100644 --- a/ai_animation/package-lock.json +++ b/ai_animation/package-lock.json @@ -1906,15 +1906,18 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", - "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" diff --git a/ai_animation/src/components/twoPowerConversation.test.ts b/ai_animation/src/components/twoPowerConversation.test.ts new file mode 100644 index 0000000..cd5cddd --- /dev/null +++ b/ai_animation/src/components/twoPowerConversation.test.ts @@ -0,0 +1,191 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +// Mock Three.js and complex dependencies first +vi.mock('three', () => ({ + WebGLRenderer: vi.fn(), + Scene: vi.fn(), + PerspectiveCamera: vi.fn(), + DirectionalLight: vi.fn(), + AmbientLight: vi.fn(), + BoxGeometry: vi.fn(), + MeshStandardMaterial: vi.fn(), + MeshBasicMaterial: vi.fn(), + Mesh: vi.fn(), + WebGLRenderTarget: vi.fn(), +})); + +vi.mock('three/examples/jsm/Addons.js', () => ({ + OrbitControls: vi.fn() +})); + +vi.mock('@tweenjs/tween.js', () => ({ + Tween: vi.fn(), + Group: vi.fn() +})); + +// Mock gameState with minimal implementation +vi.mock('../gameState', () => { + const mockEventQueue = { + pendingEvents: [], + scheduleDelay: vi.fn(), + reset: vi.fn(), + start: vi.fn(), + stop: vi.fn() + }; + + return { + gameState: { + isPlaying: false, + isDisplayingMoment: false, + phaseIndex: 0, + gameData: { + phases: [{ + name: 'S1901M', + messages: [] + }] + }, + eventQueue: mockEventQueue + } + }; +}); + +// Mock config +vi.mock('../config', () => ({ + config: { + conversationModalDelay: 500, + conversationMessageDisplay: 1000, + conversationMessageAnimation: 300, + conversationFinalDelay: 2000 + } +})); + +// Mock utils +vi.mock('../utils/powerNames', () => ({ + getPowerDisplayName: vi.fn(power => power) +})); + +// Mock the phase module to avoid circular dependency +const mockSetPhase = vi.fn(); +vi.mock('../phase', () => ({ + _setPhase: mockSetPhase +})); + +// Setup minimal DOM mocks +Object.defineProperty(global, 'document', { + value: { + createElement: vi.fn(() => ({ + style: {}, + appendChild: vi.fn(), + addEventListener: vi.fn(), + querySelector: vi.fn(() => null), + parentNode: { removeChild: vi.fn() }, + classList: { add: vi.fn() }, + textContent: '', + id: '' + })), + getElementById: vi.fn(() => null), + body: { appendChild: vi.fn() }, + removeEventListener: vi.fn(), + addEventListener: vi.fn() + } +}); + +// Import after all mocking +import { showTwoPowerConversation, closeTwoPowerConversation } from './twoPowerConversation'; + +// Get direct reference to the mocked gameState +let gameState: any; + +describe('twoPowerConversation', () => { + beforeEach(async () => { + // Get the mocked gameState + const gameStateModule = await import('../gameState'); + gameState = gameStateModule.gameState; + + // Reset mocked game state + gameState.isPlaying = false; + gameState.isDisplayingMoment = false; + gameState.phaseIndex = 0; + gameState.gameData.phases[0].messages = []; + + // Reset all mocks + vi.clearAllMocks(); + gameState.eventQueue.pendingEvents = []; + }); + + describe('showTwoPowerConversation', () => { + it('should throw error when no messages found (indicates data quality issue)', () => { + gameState.isPlaying = true; + + expect(() => { + showTwoPowerConversation({ + power1: 'FRANCE', + power2: 'GERMANY' + }); + }).toThrow('High-interest moment detected between FRANCE and GERMANY but no messages found'); + }); + + it('should throw error when empty messages array provided', () => { + gameState.isPlaying = false; + + expect(() => { + showTwoPowerConversation({ + power1: 'FRANCE', + power2: 'GERMANY', + messages: [] + }); + }).toThrow('High-interest moment detected between FRANCE and GERMANY but no messages found'); + }); + + it('should schedule phase advancement when messages exist and game is playing', () => { + gameState.isPlaying = true; + gameState.gameData.phases[0].messages = [ + { sender: 'FRANCE', recipient: 'GERMANY', message: 'Hello', time_sent: '1' } + ]; + + showTwoPowerConversation({ + power1: 'FRANCE', + power2: 'GERMANY' + }); + + // Should have scheduled events in the queue + expect(gameState.eventQueue.scheduleDelay).toHaveBeenCalled(); + + // Should have marked as displaying moment + expect(gameState.isDisplayingMoment).toBe(true); + }); + }); + + describe('Event queue safety', () => { + it('should schedule events when messages exist', () => { + gameState.isPlaying = true; + gameState.gameData.phases[0].messages = [ + { sender: 'FRANCE', recipient: 'GERMANY', message: 'Test', time_sent: '1' } + ]; + + showTwoPowerConversation({ + power1: 'FRANCE', + power2: 'GERMANY' + }); + + // Should have scheduled events in the queue + expect(gameState.eventQueue.scheduleDelay).toHaveBeenCalled(); + + // Should have marked as displaying moment + expect(gameState.isDisplayingMoment).toBe(true); + }); + + it('should throw error for empty message arrays as they indicate data quality issues', () => { + gameState.isPlaying = true; + + // Test with empty messages - should throw error + expect(() => { + showTwoPowerConversation({ + power1: 'FRANCE', + power2: 'GERMANY', + messages: [] + }); + }).toThrow('High-interest moment detected between FRANCE and GERMANY but no messages found'); + }); + }); +}); \ No newline at end of file diff --git a/ai_animation/src/components/twoPowerConversation.ts b/ai_animation/src/components/twoPowerConversation.ts index 8f14d21..05c92ef 100644 --- a/ai_animation/src/components/twoPowerConversation.ts +++ b/ai_animation/src/components/twoPowerConversation.ts @@ -37,8 +37,10 @@ export function showTwoPowerConversation(options: TwoPowerDialogueOptions): void const conversationMessages = messages || getMessagesBetweenPowers(power1, power2); if (conversationMessages.length === 0) { - console.warn(`No messages found between ${power1} and ${power2}`); - return; + throw new Error( + `High-interest moment detected between ${power1} and ${power2} but no messages found. ` + + `This indicates a data quality issue - moments should only be created when there are actual conversations to display.` + ); } // Mark as displaying moment immediately @@ -99,11 +101,11 @@ function scheduleMessageSequence( power1: string, power2: string ): void { - let currentDelay = 500; // Start after modal is fully visible + let currentDelay = config.conversationModalDelay; // Start after modal is fully visible - // Calculate timing based on mode - const messageDisplayTime = config.isInstantMode ? 100 : config.effectivePlaybackSpeed; - const messageAnimationTime = config.isInstantMode ? 50 : 300; + // Calculate timing from config + const messageDisplayTime = config.conversationMessageDisplay; + const messageAnimationTime = config.conversationMessageAnimation; messages.forEach((message, index) => { // Schedule each message display @@ -116,7 +118,7 @@ function scheduleMessageSequence( }); // Schedule conversation close after all messages are shown - const totalConversationTime = currentDelay + (config.isInstantMode ? 500 : 2000); // Extra delay before closing + const totalConversationTime = currentDelay + config.conversationFinalDelay; // Extra delay before closing gameState.eventQueue.scheduleDelay(totalConversationTime, () => { closeTwoPowerConversation(); diff --git a/ai_animation/src/config.ts b/ai_animation/src/config.ts index 3f19f25..d8d0090 100644 --- a/ai_animation/src/config.ts +++ b/ai_animation/src/config.ts @@ -55,17 +55,81 @@ export const config = { }, /** - * Get effective animation duration (0 if instant mode, normal duration otherwise) + * Get effective animation duration (minimal if instant mode, normal duration otherwise) */ get effectiveAnimationDuration(): number { - return this.isInstantMode ? 0 : this.animationDuration; + return this.isInstantMode ? 1 : this.animationDuration; }, /** * Get effective playback speed (minimal if instant mode, normal speed otherwise) */ get effectivePlaybackSpeed(): number { - return this.isInstantMode ? 10 : this.streamingPlaybackSpeed; + return this.isInstantMode ? 1 : this.streamingPlaybackSpeed; + }, + + // ======================================== + // CENTRALIZED TIMING CONFIGURATION + // All timing values should be accessed through these properties + // ======================================== + + /** + * Message display timing + */ + get messageWordDelay(): number { + return this.isInstantMode ? 1 : 50; // Delay between words in a message + }, + + get messageBetweenDelay(): number { + return this.isInstantMode ? 1 : this.effectivePlaybackSpeed; // Delay between messages + }, + + get messageCompletionDelay(): number { + return this.isInstantMode ? 1 : Math.min(this.effectivePlaybackSpeed / 3, 150); // Delay after message completion + }, + + /** + * UI Animation timing + */ + get uiFadeDelay(): number { + return this.isInstantMode ? 1 : 300; // Banner fade, overlay transitions, etc. + }, + + get uiTransitionDuration(): number { + return this.isInstantMode ? 0 : 0.3; // CSS transition duration in seconds + }, + + /** + * Two-power conversation timing + */ + get conversationMessageDisplay(): number { + return this.isInstantMode ? 50 : this.effectivePlaybackSpeed; // How long each message shows + }, + + get conversationMessageAnimation(): number { + return this.isInstantMode ? 25 : 300; // Animation time for message appearance + }, + + get conversationFinalDelay(): number { + return this.isInstantMode ? 200 : 2000; // Final delay before closing conversation + }, + + get conversationModalDelay(): number { + return this.isInstantMode ? 50 : 500; // Initial delay before showing messages + }, + + /** + * Phase and moment timing + */ + get momentDisplayTimeout(): number { + return this.isInstantMode ? 1000 : 30000; // How long moments display (though this is now managed by conversation) + }, + + /** + * Speech timing (when speech is disabled in instant mode) + */ + get speechDelay(): number { + return this.isInstantMode ? 1 : 2000; // Delay before speech starts }, // Animation timing configuration diff --git a/ai_animation/src/domElements/chatWindows.ts b/ai_animation/src/domElements/chatWindows.ts index 058224a..a2be0c2 100644 --- a/ai_animation/src/domElements/chatWindows.ts +++ b/ai_animation/src/domElements/chatWindows.ts @@ -203,8 +203,8 @@ export function updateChatWindows(stepMessages = false) { console.log(`Found ${relevantMessages.length} messages for player ${gameState.currentPower} in phase ${gameState.currentPhase.name}`); } - if (!stepMessages || config.isInstantMode) { - // Normal mode or instant chat mode: show all messages at once + if (!stepMessages) { + // Normal mode: show all messages at once relevantMessages.forEach(msg => { const isNew = addMessageToChat(msg); if (isNew) { @@ -267,7 +267,7 @@ export function updateChatWindows(stepMessages = false) { index++; // Only increment after animation completes // Schedule next message with proper delay - gameState.eventQueue.scheduleDelay(config.effectivePlaybackSpeed, showNext, `show-next-message-${index}-${Date.now()}`); + gameState.eventQueue.scheduleDelay(config.messageBetweenDelay, showNext, `show-next-message-${index}-${Date.now()}`); }; // Add the message with word animation @@ -409,7 +409,7 @@ function animateMessageWords(message: string, contentSpanId: string, targetPower console.log(`Finished animating message with ${words.length} words in ${targetPower} chat`); // Add a slight delay after the last word for readability - gameState.eventQueue.scheduleDelay(Math.min(config.effectivePlaybackSpeed / 3, 150), () => { + gameState.eventQueue.scheduleDelay(config.messageCompletionDelay, () => { if (onComplete) { onComplete(); // Call the completion callback } @@ -430,9 +430,8 @@ function animateMessageWords(message: string, contentSpanId: string, targetPower // Calculate delay based on word length and playback speed // Longer words get slightly longer display time const wordLength = words[wordIndex - 1].length; - // In streaming mode, use a more consistent delay to prevent overlap - const baseDelay = config.effectivePlaybackSpeed - const delay = Math.max(50, Math.min(200, baseDelay * (wordLength / 4))); + // Use consistent word delay from config + const delay = Math.max(config.messageWordDelay, Math.min(200, config.messageWordDelay * (wordLength / 4))); gameState.eventQueue.scheduleDelay(delay, addNextWord, `add-word-${wordIndex}-${Date.now()}`); // Scroll to ensure newest content is visible @@ -744,12 +743,12 @@ export function addToNewsBanner(newText: string): void { console.log(`Adding to news banner: "${newText}"`); } - // Add a fade-out transition (instant in instant mode) - const transitionDuration = config.isInstantMode ? 0 : 0.3; + // Add a fade-out transition + const transitionDuration = config.uiTransitionDuration; bannerEl.style.transition = `opacity ${transitionDuration}s ease-out`; bannerEl.style.opacity = '0'; - gameState.eventQueue.scheduleDelay(config.isInstantMode ? 0 : 300, () => { + gameState.eventQueue.scheduleDelay(config.uiFadeDelay, () => { // If the banner only has the default text or is empty, replace it if ( bannerEl.textContent?.trim() === 'Diplomatic actions unfolding...' || diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index ed84ad8..0152ce7 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -17,7 +17,7 @@ import { startBackgroundAudio, stopBackgroundAudio } from "./backgroundAudio"; const MOMENT_THRESHOLD = 8.0 // If we're in debug mode or instant mode, show it quick, otherwise show it for 30 seconds -const MOMENT_DISPLAY_TIMEOUT_MS = config.isDebugMode || config.isInstantMode ? 100 : 30000 +const MOMENT_DISPLAY_TIMEOUT_MS = config.isDebugMode ? 100 : config.momentDisplayTimeout // FIXME: Going to previous phases is borked. Units do not animate properly, map doesn't update. export function _setPhase(phaseIndex: number) { @@ -320,7 +320,7 @@ export function advanceToNextPhase() { console.log(`Processing phase transition for ${currentPhase.name}`); } - const speechDelay = 2000 + const speechDelay = config.speechDelay // First show summary if available if (currentPhase.summary && currentPhase.summary.trim() !== '') { diff --git a/ai_animation/src/speech.ts b/ai_animation/src/speech.ts index 6d36dda..c4ff033 100644 --- a/ai_animation/src/speech.ts +++ b/ai_animation/src/speech.ts @@ -67,12 +67,8 @@ async function testElevenLabsKey() { * @returns Promise that resolves when audio completes or rejects on error */ export async function speakSummary(summaryText: string): Promise { - if (!config.speechEnabled || config.isInstantMode) { - if (config.isInstantMode) { - console.log("Instant mode enabled, skipping TTS"); - } else { - console.log("Speech disabled via config, skipping TTS"); - } + if (!config.speechEnabled) { + console.log("Speech disabled via config, skipping TTS"); return; } diff --git a/ai_animation/tests/e2e/message-flow-verification.spec.ts b/ai_animation/tests/e2e/message-flow-verification.spec.ts new file mode 100644 index 0000000..cbcbcf9 --- /dev/null +++ b/ai_animation/tests/e2e/message-flow-verification.spec.ts @@ -0,0 +1,178 @@ +import { test, expect, Page } from '@playwright/test'; +import { waitForGameReady, getCurrentPhaseName, enableInstantMode, getAllChatMessages, waitForMessagesToComplete } from './test-helpers'; + +interface MessageRecord { + content: string; + chatWindow: string; + phase: string; + timestamp: number; +} + +/** + * Gets expected messages for current power from the browser's game data + */ +async function getExpectedMessagesFromBrowser(page: Page): Promise> { + return await page.evaluate(() => { + const gameData = window.gameState?.gameData; + const currentPower = window.gameState?.currentPower; + + if (!gameData || !currentPower) return []; + + const relevantMessages: Array<{ + sender: string; + recipient: string; + message: string; + phase: string; + }> = []; + + gameData.phases.forEach((phase: any) => { + if (phase.messages) { + phase.messages.forEach((msg: any) => { + // Apply same filtering logic as updateChatWindows() + if (msg.sender === currentPower || + msg.recipient === currentPower || + msg.recipient === 'GLOBAL') { + relevantMessages.push({ + sender: msg.sender, + recipient: msg.recipient, + message: msg.message, + phase: phase.name + }); + } + }); + } + }); + + return relevantMessages; + }); +} + + + +test.describe('Message Flow Verification', () => { + test('should verify basic message system functionality', async ({ page }) => { + // This test verifies the message system works and doesn't get stuck + await page.goto('http://localhost:5173'); + await waitForGameReady(page); + + // Enable instant mode for faster testing + await enableInstantMode(page); + + // Verify game state is accessible + const gameState = await page.evaluate(() => ({ + hasGameData: !!window.gameState?.gameData, + currentPower: window.gameState?.currentPower, + phaseIndex: window.gameState?.phaseIndex, + hasEventQueue: !!window.gameState?.eventQueue + })); + + expect(gameState.hasGameData).toBe(true); + expect(gameState.currentPower).toBeTruthy(); + expect(gameState.hasEventQueue).toBe(true); + + console.log(`Game loaded with current power: ${gameState.currentPower}`); + + // Start playback for a short time to verify message system works + await page.click('#play-btn'); + + // Monitor for basic functionality over 10 seconds + let messageAnimationDetected = false; + let eventQueueActive = false; + + for (let i = 0; i < 100; i++) { // 10 seconds in 100ms intervals + const status = await page.evaluate(() => ({ + isAnimating: window.gameState?.messagesPlaying || false, + hasEvents: window.gameState?.eventQueue?.pendingEvents?.length > 0 || false, + phase: document.querySelector('#phase-display')?.textContent?.replace('Era: ', '') || '' + })); + + if (status.isAnimating) { + messageAnimationDetected = true; + } + + if (status.hasEvents) { + eventQueueActive = true; + } + + // If we've detected both, we can finish early + if (messageAnimationDetected && eventQueueActive) { + break; + } + + await page.waitForTimeout(100); + } + + // Stop playback + await page.click('#play-btn'); + + // Verify basic functionality was detected + console.log(`Message animation detected: ${messageAnimationDetected}`); + console.log(`Event queue active: ${eventQueueActive}`); + + // At minimum, the event queue should be active (even if no messages in first phase) + expect(eventQueueActive).toBe(true); + + console.log('✅ Basic message system functionality verified'); + }); + + test('should verify no simultaneous message animations', async ({ page }) => { + await page.goto('http://localhost:5173'); + await waitForGameReady(page); + + // Enable instant mode for faster testing + await enableInstantMode(page); + + let simultaneousAnimationDetected = false; + let animationCount = 0; + + // Start playback + await page.click('#play-btn'); + + // Monitor animation state for overlaps + for (let i = 0; i < 100; i++) { // 10 seconds + const animationStatus = await page.evaluate(() => { + // Check if multiple animation systems are active simultaneously + const messagesPlaying = window.gameState?.messagesPlaying || false; + const chatMessages = document.querySelectorAll('.chat-message'); + const recentlyAdded = Array.from(chatMessages).filter(msg => { + const timeStamp = msg.dataset.timestamp; + return timeStamp && (Date.now() - parseInt(timeStamp)) < 500; // Added in last 500ms + }); + + return { + messagesPlaying, + messageCount: chatMessages.length, + recentMessages: recentlyAdded.length + }; + }); + + if (animationStatus.messagesPlaying) { + animationCount++; + + // Check if too many messages appear simultaneously (could indicate race condition) + if (animationStatus.recentMessages > 3) { + simultaneousAnimationDetected = true; + console.warn(`Potential simultaneous animation: ${animationStatus.recentMessages} recent messages`); + } + } + + await page.waitForTimeout(100); + } + + // Stop playback + await page.click('#play-btn'); + + console.log(`Animation cycles detected: ${animationCount}`); + console.log(`Simultaneous animations detected: ${simultaneousAnimationDetected}`); + + // We should see some animations but no simultaneous ones + expect(simultaneousAnimationDetected).toBe(false); + + console.log('✅ No simultaneous message animations detected'); + }); +}); \ No newline at end of file diff --git a/ai_animation/tests/e2e/test-helpers.ts b/ai_animation/tests/e2e/test-helpers.ts index 5774b82..0c89427 100644 --- a/ai_animation/tests/e2e/test-helpers.ts +++ b/ai_animation/tests/e2e/test-helpers.ts @@ -283,3 +283,149 @@ export async function advanceGameManually( return false; // Didn't reach victory } + +/** + * Helper function to wait for messages to complete animating in current phase + */ +export async function waitForMessagesToComplete(page: Page, timeoutMs = 10000): Promise { + const startTime = Date.now(); + + while (Date.now() - startTime < timeoutMs) { + const isAnimating = await page.evaluate(() => window.gameState?.messagesPlaying || false); + + if (!isAnimating) { + return; // Messages completed + } + + await page.waitForTimeout(100); + } + + throw new Error('Timeout waiting for messages to complete animation'); +} + +/** + * Helper function to get current power that the player is assigned + */ +export async function getCurrentPower(page: Page): Promise { + try { + return await page.evaluate(() => window.gameState?.currentPower || null); + } catch { + return null; + } +} + +/** + * Helper function to count visible chat messages across all chat windows + */ +export async function countVisibleChatMessages(page: Page): Promise { + try { + return await page.evaluate(() => { + const chatMessages = document.querySelectorAll('.chat-message'); + return chatMessages.length; + }); + } catch { + return 0; + } +} + +/** + * Helper function to get all chat messages with their metadata + */ +export async function getAllChatMessages(page: Page): Promise> { + try { + return await page.evaluate(() => { + const messages: Array<{ + content: string; + chatWindow: string; + phase: string; + }> = []; + + const chatMessages = document.querySelectorAll('.chat-message'); + chatMessages.forEach(messageElement => { + const chatWindow = messageElement.closest('.chat-window'); + const chatWindowId = chatWindow?.id || 'unknown'; + + // Extract message content (excluding sender label) + const contentSpan = messageElement.querySelector('span:not([class*="power-"])'); + const timeDiv = messageElement.querySelector('.message-time'); + + if (contentSpan && timeDiv) { + messages.push({ + content: contentSpan.textContent || '', + chatWindow: chatWindowId, + phase: timeDiv.textContent || '' + }); + } + }); + + return messages; + }); + } catch { + return []; + } +} + +/** + * Helper function to enable instant mode for faster testing + */ +export async function enableInstantMode(page: Page): Promise { + await page.evaluate(() => { + if (window.config) { + window.config.setInstantMode(true); + } + }); +} + +/** + * Helper function to check if the event queue has any pending events + */ +export async function hasEventQueueEvents(page: Page): Promise { + try { + return await page.evaluate(() => { + return window.gameState?.eventQueue?.pendingEvents?.length > 0 || false; + }); + } catch { + return false; + } +} + +/** + * Helper function to monitor message animation state continuously + * Returns a function to call to get the animation history + */ +export async function startMessageAnimationMonitoring(page: Page): Promise<() => Promise>> { + // Set up monitoring in the browser context + await page.evaluate(() => { + window.messageAnimationHistory = []; + + const monitor = () => { + const isAnimating = window.gameState?.messagesPlaying || false; + const phase = document.querySelector('#phase-display')?.textContent?.replace('Era: ', '') || ''; + const messageCount = document.querySelectorAll('.chat-message').length; + + window.messageAnimationHistory.push({ + timestamp: Date.now(), + isAnimating, + phase, + messageCount + }); + }; + + // Monitor every 50ms + window.messageAnimationInterval = setInterval(monitor, 50); + }); + + // Return function to get the history + return async () => { + return await page.evaluate(() => window.messageAnimationHistory || []); + }; +}