diff --git a/cypress.config.ts b/cypress.config.ts index 290f1c5e..1442c75d 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -15,10 +15,6 @@ export default defineConfig({ APPFLOWY_WS_BASE_URL: process.env.APPFLOWY_WS_BASE_URL || 'ws://localhost/ws/v2', GOTRUE_ADMIN_EMAIL: process.env.GOTRUE_ADMIN_EMAIL || 'admin@example.com', GOTRUE_ADMIN_PASSWORD: process.env.GOTRUE_ADMIN_PASSWORD || 'password', - // WebSocket mocking configuration - MOCK_WEBSOCKET: process.env.MOCK_WEBSOCKET === 'true' || false, - WS_AUTO_RESPOND: process.env.WS_AUTO_RESPOND === 'true' || false, - WS_RESPONSE_DELAY: process.env.WS_RESPONSE_DELAY || '100', }, e2e: { chromeWebSecurity: false, @@ -38,10 +34,6 @@ export default defineConfig({ config.env.APPFLOWY_WS_BASE_URL = process.env.APPFLOWY_WS_BASE_URL || config.env.APPFLOWY_WS_BASE_URL; config.env.GOTRUE_ADMIN_EMAIL = process.env.GOTRUE_ADMIN_EMAIL || config.env.GOTRUE_ADMIN_EMAIL; config.env.GOTRUE_ADMIN_PASSWORD = process.env.GOTRUE_ADMIN_PASSWORD || config.env.GOTRUE_ADMIN_PASSWORD; - // Pass WebSocket mock configuration - config.env.MOCK_WEBSOCKET = process.env.MOCK_WEBSOCKET === 'true' || config.env.MOCK_WEBSOCKET; - config.env.WS_AUTO_RESPOND = process.env.WS_AUTO_RESPOND === 'true' || config.env.WS_AUTO_RESPOND; - config.env.WS_RESPONSE_DELAY = process.env.WS_RESPONSE_DELAY || config.env.WS_RESPONSE_DELAY; // Add task for logging to Node.js console on('task', { diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 83e055ac..b018db01 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -6,10 +6,6 @@ import './auth-utils'; import './page-utils'; // Import console logger v2 (improved version) import './console-logger'; -// Import WebSocket mock utilities -import './websocket-mock'; -// Import WebSocket collab mock for document sync -import './websocket-collab-mock'; // *********************************************** // This example commands.ts shows you how to diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index c6b0e3c2..ed5709b9 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -18,33 +18,11 @@ import 'cypress-file-upload'; import 'cypress-plugin-api'; import 'cypress-real-events'; import './commands'; -import { CollabWebSocketMock } from './websocket-collab-mock'; - - -// Install WebSocket mock before window loads if enabled -if (Cypress.env('MOCK_WEBSOCKET') === true || Cypress.env('MOCK_WEBSOCKET') === 'true') { - const delay = parseInt(Cypress.env('WS_RESPONSE_DELAY') || '50'); - - Cypress.on('window:before:load', (win) => { - // Install mock on every window load - if (!(win as any).__collabMockInstance) { - (win as any).__collabMockInstance = new CollabWebSocketMock(win as any, delay); - // eslint-disable-next-line no-console - console.log('[E2E] Collab WebSocket mock installed on window:', win.location.href); - } - }); -} // Global hooks for console logging beforeEach(() => { // Start capturing console logs for each test cy.startConsoleCapture(); - - // Log if WebSocket mocking is enabled - if (Cypress.env('MOCK_WEBSOCKET') === true || Cypress.env('MOCK_WEBSOCKET') === 'true') { - cy.task('log', 'Collab WebSocket mocking enabled for this test'); - cy.log('Collab WebSocket mocking enabled'); - } }); afterEach(() => { @@ -54,11 +32,6 @@ afterEach(() => { // Stop capturing to clean up cy.stopConsoleCapture(); - - // Restore WebSocket if it was mocked - if (Cypress.env('MOCK_WEBSOCKET') === true || Cypress.env('MOCK_WEBSOCKET') === 'true') { - cy.restoreCollabWebSocket(); - } }); // Globally ignore transient app bootstrap errors during tests diff --git a/cypress/support/page/flows.ts b/cypress/support/page/flows.ts index 30803f22..370ed2ef 100644 --- a/cypress/support/page/flows.ts +++ b/cypress/support/page/flows.ts @@ -204,20 +204,9 @@ export function createPage(pageName: string) { // Check if there are any WebSocket connection indicators const wsConnected = win.localStorage.getItem('ws_connected'); cy.task('log', `WebSocket connection status: ${wsConnected || 'unknown'}`); + // Log any global WebSocket state if available + cy.task('log', `Window has WebSocket: ${!!win.WebSocket}`); }); - - if (Cypress.env('MOCK_WEBSOCKET')) { - cy.task('log', 'Waiting for document to sync (MOCK_WEBSOCKET mode)'); - cy.waitForDocumentSync(); - cy.task('log', 'Document synced'); - } else { - cy.task('log', 'WebSocket mode: Real-time sync expected'); - // Check if WebSocket is connected - cy.window().then((win) => { - // Log any global WebSocket state if available - cy.task('log', `Window has WebSocket: ${!!win.WebSocket}`); - }); - } // Wait for document to load properly - be more generous with WebSocket sync const isCi = Cypress.env('CI') || Cypress.env('GITHUB_ACTIONS'); diff --git a/cypress/support/websocket-collab-mock.ts b/cypress/support/websocket-collab-mock.ts deleted file mode 100644 index cbfb84e3..00000000 --- a/cypress/support/websocket-collab-mock.ts +++ /dev/null @@ -1,381 +0,0 @@ -/// -import * as Y from 'yjs'; -import { messages } from '../../src/proto/messages'; -// Keys expected by the app Yjs utils -const YjsEditorKey = { - data_section: 'data_section', - document: 'document', - meta: 'meta', - blocks: 'blocks', - page_id: 'page_id', - children_map: 'children_map', - text_map: 'text_map', -}; - -/** - * Enhanced WebSocket mock that simulates AppFlowy's collab protocol - * This mock ensures that the Document component receives the necessary - * Y.Doc structure to render properly during tests. - */ - -// Message types from AppFlowy protocol -interface CollabMessage { - objectId: string; - collabType: number; - syncRequest?: { - stateVector?: Uint8Array; - lastMessageId?: { timestamp: number; counter: number }; - }; - update?: { - flags: number; - payload: Uint8Array; - messageId?: { timestamp: number; counter: number }; - }; - awarenessUpdate?: { - payload: Uint8Array; - }; -} - -interface ProtobufMessage { - collabMessage?: CollabMessage; -} - -// Helper to create initial document structure -function createInitialDocumentUpdate(objectId: string): Uint8Array { - // Create a Y.Doc with the expected structure - const doc = new Y.Doc({ guid: objectId }); - - // Create the data_section map - const dataSection = doc.getMap(YjsEditorKey.data_section); - - // Create the document map with initial structure - const document = new Y.Map(); - - // Set page_id as a plain string (app expects string, not Y.Text) - document.set(YjsEditorKey.page_id, objectId); - - // Add blocks map for document content with a root page block - const blocks = new Y.Map(); - const rootBlock = new Y.Map(); - rootBlock.set('id', objectId); - rootBlock.set('ty', 0); // BlockType.Page - rootBlock.set('children', objectId); - rootBlock.set('data', ''); - rootBlock.set('parent', objectId); - rootBlock.set('external_id', objectId); - blocks.set(objectId, rootBlock); - - // Create a paragraph block for the editor to render - const paragraphId = `${objectId}-p1`; - const paragraphBlock = new Y.Map(); - paragraphBlock.set('id', paragraphId); - paragraphBlock.set('ty', 1); // BlockType.Paragraph - paragraphBlock.set('children', paragraphId); - paragraphBlock.set('data', '{}'); - paragraphBlock.set('parent', objectId); - paragraphBlock.set('external_id', paragraphId); - blocks.set(paragraphId, paragraphBlock); - - document.set(YjsEditorKey.blocks, blocks); - - // Add meta information - const meta = new Y.Map(); - const childrenMap = new Y.Map(); - - // Add the paragraph as a child of the root page - const rootChildrenArray = new Y.Array(); - rootChildrenArray.push([paragraphId]); - childrenMap.set(objectId, rootChildrenArray); - - // Add empty children array for the paragraph - const paragraphChildrenArray = new Y.Array(); - childrenMap.set(paragraphId, paragraphChildrenArray); - - meta.set(YjsEditorKey.children_map, childrenMap); - - // Add text for the paragraph - const textMap = new Y.Map(); - const paragraphText = new Y.Text(); - textMap.set(paragraphId, paragraphText); - meta.set(YjsEditorKey.text_map, textMap); - - document.set(YjsEditorKey.meta, meta); - - // Set the document in data_section - dataSection.set(YjsEditorKey.document, document); - - // Log the structure for debugging - console.log('[CollabWebSocketMock] Created initial document structure:', { - objectId, - hasDataSection: doc.getMap(YjsEditorKey.data_section) !== undefined, - hasDocument: dataSection.get(YjsEditorKey.document) !== undefined, - blocksCount: blocks.size, - hasRootBlock: blocks.has(objectId), - }); - - // Encode the state as an update - return Y.encodeStateAsUpdate(doc); -} - -// WebSocket mock class with collab protocol support -class CollabWebSocketMock { - private url: string; - private ws: WebSocket | null = null; - private originalWebSocket: typeof WebSocket; - private pendingDocs: Set = new Set(); - private syncedDocs: Set = new Set(); - private messageQueue: any[] = []; - private responseDelay: number; - private targetWindow: Window & typeof globalThis; - - constructor(targetWindow: Window & typeof globalThis, responseDelay: number = 50) { - this.targetWindow = targetWindow; - this.url = ''; - this.originalWebSocket = targetWindow.WebSocket; - this.responseDelay = responseDelay; - this.setupMock(); - } - - private setupMock() { - const self = this; - - // Replace WebSocket constructor on AUT window - (this.targetWindow as any).WebSocket = function (url: string, protocols?: string | string[]) { - // Check if this is the AppFlowy WebSocket URL - if (!url.includes('/ws/v2/')) { - // Use real WebSocket for non-collab URLs - return new self.originalWebSocket(url, protocols); - } - - // Intercept collab socket - self.url = url; - // Create mock WebSocket instance - const mockWs: any = { - url: url, - readyState: 0, // CONNECTING - binaryType: 'arraybuffer', - onopen: null, - onclose: null, - onerror: null, - onmessage: null, - listeners: new Map>(), - - send: (data: any) => { - if (mockWs.readyState !== 1) { - throw new Error('WebSocket is not open'); - } - // Heartbeat support: echo back plain text heartbeats - if (typeof data === 'string' && data === 'echo') { - const echoEvent = new MessageEvent('message', { data: 'echo' }); - setTimeout(() => { - mockWs.onmessage?.(echoEvent); - mockWs.dispatchEvent(echoEvent); - }, 10); - return; - } - self.handleMessage(mockWs, data); - }, - - close: (code?: number, reason?: string) => { - if (mockWs.readyState === 2 || mockWs.readyState === 3) return; - - mockWs.readyState = 2; // CLOSING - setTimeout(() => { - mockWs.readyState = 3; // CLOSED - const closeEvent = new CloseEvent('close', { - code: code || 1000, - reason: reason || '', - wasClean: true, - }); - mockWs.onclose?.(closeEvent); - mockWs.dispatchEvent(closeEvent); - }, 10); - }, - - addEventListener: (type: string, listener: EventListener) => { - if (!mockWs.listeners.has(type)) { - mockWs.listeners.set(type, new Set()); - } - mockWs.listeners.get(type)!.add(listener); - }, - - removeEventListener: (type: string, listener: EventListener) => { - mockWs.listeners.get(type)?.delete(listener); - }, - - dispatchEvent: (event: Event) => { - const listeners = mockWs.listeners.get(event.type); - if (listeners) { - listeners.forEach((listener: EventListener) => listener(event)); - } - return true; - } - }; - - // Ensure instanceof checks pass - try { - (mockWs as any).constructor = (self.targetWindow as any).WebSocket; - Object.setPrototypeOf(mockWs, (self.targetWindow as any).WebSocket.prototype); - } catch (_) { - // ignore - } - - self.ws = mockWs; - - // Store mock on AUT window for debugging - (self.targetWindow as any).__mockCollabWebSocket = mockWs; - - // Simulate connection opening - setTimeout(() => { - mockWs.readyState = 1; // OPEN - const openEvent = new Event('open'); - mockWs.onopen?.(openEvent); - mockWs.dispatchEvent(openEvent); - console.log('[CollabWebSocketMock] WebSocket connection opened for URL:', url); - - // Log what page IDs we're dealing with - const urlMatch = url.match(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g); - if (urlMatch) { - console.log('[CollabWebSocketMock] Found object IDs in URL:', urlMatch); - } - - // Process any queued messages - self.processMessageQueue(mockWs); - }, 50); - - return mockWs; - } as any; - - // Copy WebSocket static properties - Object.setPrototypeOf(this.targetWindow.WebSocket, this.originalWebSocket); - // Align prototype so `instanceof WebSocket` works - try { - (this.targetWindow as any).WebSocket.prototype = this.originalWebSocket.prototype; - } catch (_) { - // no-op - } - // Copy static constants (read-only properties) - try { - Object.defineProperty(this.targetWindow.WebSocket, 'CONNECTING', { value: 0, writable: false }); - Object.defineProperty(this.targetWindow.WebSocket, 'OPEN', { value: 1, writable: false }); - Object.defineProperty(this.targetWindow.WebSocket, 'CLOSING', { value: 2, writable: false }); - Object.defineProperty(this.targetWindow.WebSocket, 'CLOSED', { value: 3, writable: false }); - } catch (_) { - // no-op if already defined - } - } - - private handleMessage(ws: any, data: ArrayBuffer | Uint8Array | string) { - try { - // Ignore heartbeat or text messages - if (typeof data === 'string') { - return; - } - - const buffer = data instanceof ArrayBuffer - ? new Uint8Array(data) - : new Uint8Array(data.buffer, data.byteOffset, data.byteLength); - - const decoded = messages.Message.decode(buffer); - const collabMsg = decoded.collabMessage; - - if (collabMsg?.syncRequest && collabMsg.objectId) { - const objectId = collabMsg.objectId; - const collabType = collabMsg.collabType ?? 0; - setTimeout(() => { - this.sendSyncResponse(ws, objectId, collabType); - }, this.responseDelay); - } - } catch (error) { - // If decoding fails, ignore silently to avoid breaking app - // Useful when app sends non-collab binary messages - } - } - - private sendSyncResponse(ws: any, objectId: string, collabType: number) { - if (this.syncedDocs.has(objectId)) return; - - const update = createInitialDocumentUpdate(objectId); - const msg: messages.IMessage = { - collabMessage: { - objectId, - collabType, - update: { - flags: 0, - payload: update, - messageId: { timestamp: Date.now(), counter: 1 }, - }, - }, - }; - const encoded = messages.Message.encode(msg).finish(); - const dataBuf = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength); - const messageEvent = new MessageEvent('message', { data: dataBuf }); - - ws.onmessage?.(messageEvent); - ws.dispatchEvent(messageEvent); - this.syncedDocs.add(objectId); - } - - private processMessageQueue(ws: any) { - while (this.messageQueue.length > 0) { - const msg = this.messageQueue.shift(); - this.handleMessage(ws, msg); - } - } - - public restore() { - this.targetWindow.WebSocket = this.originalWebSocket; - delete (this.targetWindow as any).__mockCollabWebSocket; - this.pendingDocs.clear(); - this.syncedDocs.clear(); - } - - public addPendingDocument(objectId: string) { - this.pendingDocs.add(objectId); - } -} - -// Cypress commands for collab WebSocket mocking -declare global { - namespace Cypress { - interface Chainable { - mockCollabWebSocket(responseDelay?: number): Chainable; - restoreCollabWebSocket(): Chainable; - waitForDocumentSync(objectId?: string, timeout?: number): Chainable; - } - } -} - -// Store mock instance globally -let collabMockInstance: CollabWebSocketMock | null = null; - -// Add Cypress commands -if ((window as any).Cypress) { - Cypress.Commands.add('mockCollabWebSocket', (responseDelay = 50) => { - cy.window().then((win) => { - if (!collabMockInstance) { - collabMockInstance = new CollabWebSocketMock(win, responseDelay); - (win as any).__collabMockInstance = collabMockInstance; - } - }); - }); - - Cypress.Commands.add('restoreCollabWebSocket', () => { - cy.window().then((win) => { - if (collabMockInstance) { - collabMockInstance.restore(); - collabMockInstance = null; - delete (win as any).__collabMockInstance; - } - }); - }); - - Cypress.Commands.add('waitForDocumentSync', (objectId?: string, timeout = 10000) => { - // When mocking, just wait for the modal to be visible - // The Document component might not render immediately even with mocked data - cy.get('[role="dialog"]', { timeout: 5000 }).should('be.visible'); - cy.wait(1000); // Give time for the document to render - }); -} - -export { CollabWebSocketMock }; diff --git a/cypress/support/websocket-mock.ts b/cypress/support/websocket-mock.ts deleted file mode 100644 index 73a5520e..00000000 --- a/cypress/support/websocket-mock.ts +++ /dev/null @@ -1,269 +0,0 @@ -/// - -interface MockWebSocketConfig { - enabled: boolean; - url?: string | RegExp; - responseDelay?: number; - autoRespond?: boolean; - mockResponses?: Map; -} - -interface MockWebSocketInstance { - url: string; - readyState: number; - binaryType: BinaryType; - onopen: ((event: Event) => void) | null; - onclose: ((event: CloseEvent) => void) | null; - onerror: ((event: Event) => void) | null; - onmessage: ((event: MessageEvent) => void) | null; - send: (data: any) => void; - close: (code?: number, reason?: string) => void; - addEventListener: (type: string, listener: EventListener) => void; - removeEventListener: (type: string, listener: EventListener) => void; - dispatchEvent: (event: Event) => boolean; -} - -class MockWebSocket implements MockWebSocketInstance { - url: string; - readyState: number = 0; // CONNECTING - binaryType: BinaryType = 'arraybuffer'; - onopen: ((event: Event) => void) | null = null; - onclose: ((event: CloseEvent) => void) | null = null; - onerror: ((event: Event) => void) | null = null; - onmessage: ((event: MessageEvent) => void) | null = null; - - private listeners: Map> = new Map(); - private config: MockWebSocketConfig; - private sentMessages: any[] = []; - - constructor(url: string, protocols?: string | string[], config?: MockWebSocketConfig) { - this.url = url; - this.config = config || { enabled: true }; - - // Store the mock instance for Cypress access - if ((window as any).Cypress) { - (window as any).__mockWebSocket = this; - (window as any).__mockWebSocketMessages = this.sentMessages; - } - - // Simulate connection opening - setTimeout(() => { - this.readyState = 1; // OPEN - const openEvent = new Event('open'); - this.onopen?.(openEvent); - this.dispatchEvent(openEvent); - - // Log connection opened - console.log('[MockWebSocket] Connection opened:', url); - }, this.config.responseDelay || 100); - } - - send(data: any): void { - if (this.readyState !== 1) { - throw new Error('WebSocket is not open'); - } - - this.sentMessages.push(data); - console.log('[MockWebSocket] Message sent:', data); - - // Auto-respond with echo if configured - if (this.config.autoRespond) { - setTimeout(() => { - this.receiveMessage(data); - }, this.config.responseDelay || 50); - } - - // Check for mock responses - if (this.config.mockResponses) { - const messageStr = typeof data === 'string' ? data : JSON.stringify(data); - for (const [pattern, response] of this.config.mockResponses) { - if (messageStr.includes(pattern)) { - setTimeout(() => { - this.receiveMessage(response); - }, this.config.responseDelay || 50); - break; - } - } - } - } - - receiveMessage(data: any): void { - const messageEvent = new MessageEvent('message', { data }); - this.onmessage?.(messageEvent); - this.dispatchEvent(messageEvent); - console.log('[MockWebSocket] Message received:', data); - } - - close(code: number = 1000, reason?: string): void { - if (this.readyState === 2 || this.readyState === 3) { - return; // Already closing or closed - } - - this.readyState = 2; // CLOSING - - setTimeout(() => { - this.readyState = 3; // CLOSED - const closeEvent = new CloseEvent('close', { - code, - reason, - wasClean: true, - }); - this.onclose?.(closeEvent); - this.dispatchEvent(closeEvent); - console.log('[MockWebSocket] Connection closed:', code, reason); - }, 10); - } - - addEventListener(type: string, listener: EventListener): void { - if (!this.listeners.has(type)) { - this.listeners.set(type, new Set()); - } - this.listeners.get(type)!.add(listener); - } - - removeEventListener(type: string, listener: EventListener): void { - this.listeners.get(type)?.delete(listener); - } - - dispatchEvent(event: Event): boolean { - const listeners = this.listeners.get(event.type); - if (listeners) { - listeners.forEach(listener => listener(event)); - } - return true; - } -} - -// WebSocket mock management functions -export function setupWebSocketMock(config?: Partial): void { - const defaultConfig: MockWebSocketConfig = { - enabled: true, - autoRespond: false, - responseDelay: 100, - mockResponses: new Map(), - ...config, - }; - - if (!defaultConfig.enabled) { - console.log('[MockWebSocket] Mocking disabled'); - return; - } - - // Store original WebSocket - const OriginalWebSocket = window.WebSocket; - (window as any).__OriginalWebSocket = OriginalWebSocket; - - // Replace WebSocket with mock - (window as any).WebSocket = function(url: string, protocols?: string | string[]) { - // Check if URL matches the pattern to mock - if (defaultConfig.url) { - const shouldMock = typeof defaultConfig.url === 'string' - ? url.includes(defaultConfig.url) - : defaultConfig.url.test(url); - - if (!shouldMock) { - console.log('[MockWebSocket] URL not matched, using real WebSocket:', url); - return new OriginalWebSocket(url, protocols); - } - } - - console.log('[MockWebSocket] Creating mock WebSocket for:', url); - return new MockWebSocket(url, protocols, defaultConfig); - }; - - // Copy static properties - Object.setPrototypeOf(window.WebSocket, OriginalWebSocket); - Object.keys(OriginalWebSocket).forEach(key => { - (window.WebSocket as any)[key] = (OriginalWebSocket as any)[key]; - }); - - console.log('[MockWebSocket] WebSocket mocking enabled'); -} - -export function restoreWebSocket(): void { - if ((window as any).__OriginalWebSocket) { - window.WebSocket = (window as any).__OriginalWebSocket; - delete (window as any).__OriginalWebSocket; - delete (window as any).__mockWebSocket; - delete (window as any).__mockWebSocketMessages; - console.log('[MockWebSocket] WebSocket restored to original'); - } -} - -export function getMockWebSocketInstance(): MockWebSocketInstance | null { - return (window as any).__mockWebSocket || null; -} - -export function getMockWebSocketMessages(): any[] { - return (window as any).__mockWebSocketMessages || []; -} - -// Cypress commands -declare global { - namespace Cypress { - interface Chainable { - mockWebSocket(config?: Partial): Chainable; - restoreWebSocket(): Chainable; - getWebSocketMessages(): Chainable; - sendWebSocketMessage(data: any): Chainable; - waitForWebSocketMessage(predicate?: (msg: any) => boolean, timeout?: number): Chainable; - } - } -} - -// Add Cypress commands -if ((window as any).Cypress) { - Cypress.Commands.add('mockWebSocket', (config?: Partial) => { - cy.window().then((win) => { - (win as any).setupWebSocketMock = setupWebSocketMock; - (win as any).setupWebSocketMock(config); - }); - }); - - Cypress.Commands.add('restoreWebSocket', () => { - cy.window().then((win) => { - (win as any).restoreWebSocket = restoreWebSocket; - (win as any).restoreWebSocket(); - }); - }); - - Cypress.Commands.add('getWebSocketMessages', () => { - cy.window().then((win) => { - return (win as any).__mockWebSocketMessages || []; - }); - }); - - Cypress.Commands.add('sendWebSocketMessage', (data: any) => { - cy.window().then((win) => { - const mockWs = (win as any).__mockWebSocket; - if (mockWs) { - mockWs.receiveMessage(data); - } else { - throw new Error('No mock WebSocket instance found'); - } - }); - }); - - Cypress.Commands.add('waitForWebSocketMessage', (predicate?: (msg: any) => boolean, timeout = 5000) => { - return cy.window().then((win) => { - return new Cypress.Promise((resolve) => { - const startTime = Date.now(); - const checkMessages = () => { - const messages = (win as any).__mockWebSocketMessages || []; - const foundMessage = predicate - ? messages.find(predicate) - : messages[messages.length - 1]; - - if (foundMessage) { - resolve(foundMessage); - } else if (Date.now() - startTime > timeout) { - throw new Error('Timeout waiting for WebSocket message'); - } else { - setTimeout(checkMessages, 100); - } - }; - checkMessages(); - }); - }); - }); -} \ No newline at end of file