mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2025-11-28 18:28:02 +08:00
chore: remove mock ws
This commit is contained in:
@@ -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', {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -1,381 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
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<string>();
|
||||
rootChildrenArray.push([paragraphId]);
|
||||
childrenMap.set(objectId, rootChildrenArray);
|
||||
|
||||
// Add empty children array for the paragraph
|
||||
const paragraphChildrenArray = new Y.Array<string>();
|
||||
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<string> = new Set();
|
||||
private syncedDocs: Set<string> = 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<string, Set<EventListener>>(),
|
||||
|
||||
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<void>;
|
||||
restoreCollabWebSocket(): Chainable<void>;
|
||||
waitForDocumentSync(objectId?: string, timeout?: number): Chainable<void>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 };
|
||||
@@ -1,269 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
interface MockWebSocketConfig {
|
||||
enabled: boolean;
|
||||
url?: string | RegExp;
|
||||
responseDelay?: number;
|
||||
autoRespond?: boolean;
|
||||
mockResponses?: Map<string, any>;
|
||||
}
|
||||
|
||||
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<string, Set<EventListener>> = 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<MockWebSocketConfig>): 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<MockWebSocketConfig>): Chainable<void>;
|
||||
restoreWebSocket(): Chainable<void>;
|
||||
getWebSocketMessages(): Chainable<any[]>;
|
||||
sendWebSocketMessage(data: any): Chainable<void>;
|
||||
waitForWebSocketMessage(predicate?: (msg: any) => boolean, timeout?: number): Chainable<any>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add Cypress commands
|
||||
if ((window as any).Cypress) {
|
||||
Cypress.Commands.add('mockWebSocket', (config?: Partial<MockWebSocketConfig>) => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user