From c60d0ad235ec6e35bc8679412c08989bdba9dd26 Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Thu, 8 May 2025 16:05:31 +0200 Subject: [PATCH] test: refactor and expand unit tests for OpenViduMeet component, add style mock --- frontend/webcomponent/jest.config.mjs | 2 +- .../webcomponent/tests/__mocks__/styleMock.js | 1 + .../tests/unit/attributes.test.ts | 65 +++++++ .../webcomponent/tests/unit/commands.test.ts | 68 +++++++ .../webcomponent/tests/unit/events.test.ts | 44 +++++ .../webcomponent/tests/unit/lifecycle.test.ts | 141 +++++++++++++++ .../tests/unit/openvidu-meet.test.ts | 169 ------------------ 7 files changed, 320 insertions(+), 170 deletions(-) create mode 100644 frontend/webcomponent/tests/__mocks__/styleMock.js create mode 100644 frontend/webcomponent/tests/unit/attributes.test.ts create mode 100644 frontend/webcomponent/tests/unit/commands.test.ts create mode 100644 frontend/webcomponent/tests/unit/events.test.ts create mode 100644 frontend/webcomponent/tests/unit/lifecycle.test.ts delete mode 100644 frontend/webcomponent/tests/unit/openvidu-meet.test.ts diff --git a/frontend/webcomponent/jest.config.mjs b/frontend/webcomponent/jest.config.mjs index 82fbafe..239c361 100644 --- a/frontend/webcomponent/jest.config.mjs +++ b/frontend/webcomponent/jest.config.mjs @@ -17,7 +17,7 @@ const jestConfig = { } }, moduleNameMapper: { - '\\.(css|less|scss|sass)$': 'identity-obj-proxy' + '\\.(css|less|scss|sass)$': '/tests/__mocks__/styleMock.js' } } diff --git a/frontend/webcomponent/tests/__mocks__/styleMock.js b/frontend/webcomponent/tests/__mocks__/styleMock.js new file mode 100644 index 0000000..b0c5090 --- /dev/null +++ b/frontend/webcomponent/tests/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = '' diff --git a/frontend/webcomponent/tests/unit/attributes.test.ts b/frontend/webcomponent/tests/unit/attributes.test.ts new file mode 100644 index 0000000..cd6ee51 --- /dev/null +++ b/frontend/webcomponent/tests/unit/attributes.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, jest } from '@jest/globals'; +import { WEBCOMPONENT_ROOM_URL } from '../config'; +import { OpenViduMeet } from '../../src/components/OpenViduMeet'; +import '../../src/index'; + +describe('Web Component Attributes', () => { + let component: OpenViduMeet; + + beforeEach(() => { + component = document.createElement('openvidu-meet') as OpenViduMeet; + document.body.appendChild(component); + }); + + afterEach(() => { + document.body.removeChild(component); + document.body.innerHTML = ''; + }); + + it('should render iframe with correct attributes', () => { + const iframe = component.shadowRoot?.querySelector('iframe'); + expect(iframe).not.toBeNull(); + expect(iframe?.getAttribute('allow')).toContain('camera'); + expect(iframe?.getAttribute('allow')).toContain('microphone'); + expect(iframe?.getAttribute('allow')).toContain('fullscreen'); + expect(iframe?.getAttribute('allow')).toContain('display-capture'); + }); + + it('should reject rendering iframe when "room-url" attribute is missing', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Trigger updateIframeSrc manually + (component as any).updateIframeSrc(); + + const iframe = component.shadowRoot?.querySelector('iframe'); + + expect(iframe).toBeDefined(); + expect(iframe?.src).toBeFalsy(); + expect(consoleErrorSpy).toHaveBeenCalledWith('The "room-url" attribute is required.'); + + consoleErrorSpy.mockRestore(); + }); + + it('should update iframe src when "room-url" attribute changes', () => { + component.setAttribute('room-url', WEBCOMPONENT_ROOM_URL); + component.setAttribute('user', 'testUser'); + + // Manually trigger the update (MutationObserver doesn't always trigger in tests) + (component as any).updateIframeSrc(); + + const iframe = component.shadowRoot?.querySelector('iframe'); + expect(iframe?.src).toEqual(`${WEBCOMPONENT_ROOM_URL}?user=testUser`); + }); + + it('should extract origin from room-url and set as allowed origin', () => { + const roomUrl = 'https://example.com/room/123'; + component.setAttribute('room-url', roomUrl); + + // Trigger update + (component as any).updateIframeSrc(); + + // Check if origin was extracted and set + expect((component as any).allowedOrigin).toBe('https://example.com'); + expect((component as any).commandsManager.allowedOrigin).toBe('https://example.com'); + }); +}); diff --git a/frontend/webcomponent/tests/unit/commands.test.ts b/frontend/webcomponent/tests/unit/commands.test.ts new file mode 100644 index 0000000..14b4789 --- /dev/null +++ b/frontend/webcomponent/tests/unit/commands.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect, jest } from '@jest/globals'; +import { OpenViduMeet } from '../../src/components/OpenViduMeet'; +import '../../src/index'; +import { WebComponentCommand } from '../../src/models/command.model'; + +describe('OpenViduMeet Web Component Commands', () => { + let component: OpenViduMeet; + + beforeEach(() => { + component = document.createElement('openvidu-meet') as OpenViduMeet; + document.body.appendChild(component); + }); + + afterEach(() => { + document.body.removeChild(component); + jest.restoreAllMocks(); + document.body.innerHTML = ''; + }); + + it('should update allowedOrigin when setAllowedOrigin is called', () => { + const testOrigin = 'https://test-origin.com'; + + // Set allowed origin + (component as any).commandsManager.setAllowedOrigin(testOrigin); + + // Check if it was updated + expect((component as any).commandsManager.allowedOrigin).toBe(testOrigin); + }); + + it('should call commandsManager.sendMessage when leaveRoom is called', () => { + const spy = jest.spyOn(component['commandsManager'], 'sendMessage'); + component.leaveRoom(); + expect(spy).toHaveBeenCalledWith({ command: WebComponentCommand.LEAVE_ROOM }); + }); + + it('should call commandsManager.sendMessage when endMeeting is called', () => { + const spy = jest.spyOn(component['commandsManager'], 'sendMessage'); + component.endMeeting(); + expect(spy).toHaveBeenCalledWith({ command: WebComponentCommand.END_MEETING }); + }); + + it('should send message to iframe with correct origin', () => { + // Mock iframe contentWindow and postMessage + const mockPostMessage = jest.fn(); + const iframe = component.shadowRoot?.querySelector('iframe'); + + // Mock the contentWindow + Object.defineProperty(iframe, 'contentWindow', { + value: { postMessage: mockPostMessage }, + writable: true + }); + + // Set allowed origin + (component as any).commandsManager.setAllowedOrigin('https://test.com'); + + // Send a message + (component as any).commandsManager.sendMessage({ command: 'TEST' }); + + // Check if postMessage was called correctly + expect(mockPostMessage).toHaveBeenCalledWith({ command: 'TEST' }, 'https://test.com'); + }); + + // it('should call commandsManager.sendMessage when toggleChat is called', () => { + // const spy = jest.spyOn(component['commandsManager'], 'sendMessage'); + // component.toggleChat(); + // expect(spy).toHaveBeenCalledWith({ action: WebComponentCommand.TOGGLE_CHAT }); + // }); +}); diff --git a/frontend/webcomponent/tests/unit/events.test.ts b/frontend/webcomponent/tests/unit/events.test.ts new file mode 100644 index 0000000..10d9ecb --- /dev/null +++ b/frontend/webcomponent/tests/unit/events.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect, jest } from '@jest/globals'; +import { OpenViduMeet } from '../../src/components/OpenViduMeet'; +import { EventsManager } from '../../src/components/EventsManager'; +import '../../src/index'; + +describe('Web Component Events', () => { + let component: OpenViduMeet; + let eventsManager: EventsManager; + const allowedOrigin = 'http://example.com'; + + beforeEach(() => { + component = document.createElement('openvidu-meet') as OpenViduMeet; + eventsManager = new EventsManager(component); + document.body.appendChild(component); + }); + + afterEach(() => { + document.body.removeChild(component); + document.body.innerHTML = ''; + }); + + it('should register message event listener on connection', () => { + const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + + // Call connectedCallback again (even though it was called when created) + (component as any).connectedCallback(); + + expect(addEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function)); + }); + + it('should ignore invalid messages', () => { + const dispatchEventSpy = jest.spyOn(component, 'dispatchEvent'); + const event = new MessageEvent('message', { + origin: allowedOrigin, + data: { invalid: 'data' } + }); + + (eventsManager as any).handleMessage(event); + + expect(dispatchEventSpy).not.toHaveBeenCalled(); + }); + + // TODO: Add test for leave room event +}); diff --git a/frontend/webcomponent/tests/unit/lifecycle.test.ts b/frontend/webcomponent/tests/unit/lifecycle.test.ts new file mode 100644 index 0000000..e2966c3 --- /dev/null +++ b/frontend/webcomponent/tests/unit/lifecycle.test.ts @@ -0,0 +1,141 @@ +import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +import { OpenViduMeet } from '../../src/components/OpenViduMeet'; +import '../../src/index'; +import { WebComponentCommand } from '../../src/models/command.model'; +import { WEBCOMPONENT_ROOM_URL } from '../config'; + +describe('OpenViduMeet Event Handling', () => { + let component: OpenViduMeet; + + beforeEach(() => { + component = document.createElement('openvidu-meet') as OpenViduMeet; + document.body.appendChild(component); + }); + + afterEach(() => { + document.body.removeChild(component); + jest.restoreAllMocks(); + document.body.innerHTML = ''; + }); + + it('should be created correctly', () => { + expect(component).toBeDefined(); + expect(component.shadowRoot).not.toBeNull(); + }); + + + it('should remove message event listener on disconnection', () => { + const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + + // Call disconnectedCallback + (component as any).disconnectedCallback(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function)); + }); + + it('should call sendMessage only once when iframe loads', () => { + const sendMessageSpy = jest.spyOn(component['commandsManager'], 'sendMessage'); + + const iframe = component.shadowRoot?.querySelector('iframe'); + expect(iframe).not.toBeNull(); + + // Emulate iframe load event + iframe?.dispatchEvent(new Event('load')); + + expect(sendMessageSpy).toHaveBeenCalledTimes(1); + expect(sendMessageSpy).toHaveBeenCalledWith({ + command: WebComponentCommand.INITIALIZE, + payload: { domain: window.location.origin } + }); + + // Dispatch load event again to check if sendMessage is not called again + iframe?.dispatchEvent(new Event('load')); + + // Check if sendMessage was not called again + expect(sendMessageSpy).toHaveBeenCalledTimes(1); + }); + + it('should dispatch custom events when receiving messages', () => { + // Create a spy for dispatchEvent + const dispatchEventSpy = jest.spyOn(component, 'dispatchEvent'); + + // Mock a message event + const messageEvent = new MessageEvent('message', { + data: { + event: 'test-event', + payload: { foo: 'bar' } + } + }); + + // Manually call the handler + (component as any).eventsManager.handleMessage(messageEvent); + + // Check if custom event was dispatched + expect(dispatchEventSpy).toHaveBeenCalled(); + expect(dispatchEventSpy.mock.calls[0][0].type).toBe('test-event'); + expect(dispatchEventSpy.mock.calls[0][0].bubbles).toBe(true); + expect(dispatchEventSpy.mock.calls[0][0].composed).toBe(true); + expect((dispatchEventSpy.mock.calls[0][0]as any).detail).toBeInstanceOf(Object); + expect((dispatchEventSpy.mock.calls[0][0]as any).detail).toHaveProperty('foo'); + expect((dispatchEventSpy.mock.calls[0][0]as any).detail.foo).toBe('bar'); + }); + + it('should clean up resources when removed from DOM', () => { + + // Set up spies + const clearTimeoutSpy = jest.spyOn(window, 'clearTimeout'); + const eventsCleanupSpy = jest.spyOn(component['eventsManager'], 'cleanup'); + + // Set a load timeout + (component as any).loadTimeout = setTimeout(() => {}, 1000); + + // Remove from DOM + + (component as any).disconnectedCallback(); + + // Check if cleanup was called + expect(clearTimeoutSpy).toHaveBeenCalled(); + expect(eventsCleanupSpy).toHaveBeenCalled(); + }); + + it('should re-render when showing error state', () => { + document.body.appendChild(component); + + // Get initial render state + const initialIframe = component.shadowRoot?.querySelector('iframe'); + expect(initialIframe).not.toBeNull(); + + // Simulate showing an error + (component as any).showErrorState('Test error'); + + // Check if DOM was re-rendered with error + const iframe = component.shadowRoot?.querySelector('iframe'); + expect(iframe).toBeNull(); + + const errorContainer = component.shadowRoot?.querySelector('.error-container'); + expect(errorContainer).not.toBeNull(); + expect(errorContainer?.querySelector('.error-message')?.textContent).toBe('Test error'); + }); + + it('should properly update iframe src with query parameters', () => { + document.body.appendChild(component); + + // Set attributes + component.setAttribute('room-url', WEBCOMPONENT_ROOM_URL); + component.setAttribute('user', 'testUser'); + component.setAttribute('role', 'publisher'); + component.setAttribute('token', 'test-token'); + + // Trigger update + (component as any).updateIframeSrc(); + + // Check iframe src + const iframe = component.shadowRoot?.querySelector('iframe'); + const src = iframe?.src; + + expect(src).toContain(WEBCOMPONENT_ROOM_URL); + expect(src).toContain('user=testUser'); + expect(src).toContain('role=publisher'); + expect(src).toContain('token=test-token'); + }); +}); diff --git a/frontend/webcomponent/tests/unit/openvidu-meet.test.ts b/frontend/webcomponent/tests/unit/openvidu-meet.test.ts deleted file mode 100644 index 70ff73c..0000000 --- a/frontend/webcomponent/tests/unit/openvidu-meet.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { describe, it, expect, jest } from '@jest/globals'; -import { WEBCOMPONENT_ROOM_URL } from '../config'; -import { OpenViduMeet } from '../../src/components/OpenViduMeet'; -import { EventsManager } from '../../src/components/EventsManager'; -import '../../src/index'; -import { WebComponentActionType } from '../../src/types/message.type'; - -describe('OpenVidu Meet Web Component Attributes', () => { - let component: OpenViduMeet; - - beforeEach(() => { - component = document.createElement('openvidu-meet') as OpenViduMeet; - document.body.appendChild(component); - }); - - afterEach(() => { - document.body.removeChild(component); - document.body.innerHTML = ''; - }); - - test('should be created correctly', () => { - expect(component).toBeDefined(); - expect(component.shadowRoot).not.toBeNull(); - }); - - test('should render iframe with correct attributes', () => { - const iframe = component.shadowRoot?.querySelector('iframe'); - expect(iframe).not.toBeNull(); - expect(iframe?.getAttribute('allow')).toContain('camera'); - expect(iframe?.getAttribute('allow')).toContain('microphone'); - expect(iframe?.getAttribute('allow')).toContain('fullscreen'); - expect(iframe?.getAttribute('allow')).toContain('display-capture'); - }); - - test('should reject rendering iframe when "room-url" attribute is missing', () => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - - // Trigger updateIframeSrc manually - (component as any).updateIframeSrc(); - - const iframe = component.shadowRoot?.querySelector('iframe'); - - expect(iframe).toBeDefined(); - expect(iframe?.src).toBeFalsy(); - expect(consoleErrorSpy).toHaveBeenCalledWith('The "room-url" attribute is required.'); - - consoleErrorSpy.mockRestore(); - }); - - test('should update iframe src when "room-url" attribute changes', () => { - component.setAttribute('room-url', WEBCOMPONENT_ROOM_URL); - component.setAttribute('user', 'testUser'); - - // Manually trigger the update (MutationObserver doesn't always trigger in tests) - (component as any).updateIframeSrc(); - - const iframe = component.shadowRoot?.querySelector('iframe'); - expect(iframe?.src).toEqual(`${WEBCOMPONENT_ROOM_URL}?user=testUser`); - }); -}); - -describe('OpenViduMeet Web Component Events Listener', () => { - let component: OpenViduMeet; - let eventsManager: EventsManager; - const allowedOrigin = 'http://example.com'; - - beforeEach(() => { - component = document.createElement('openvidu-meet') as OpenViduMeet; - eventsManager = new EventsManager(component); - document.body.appendChild(component); - }); - - afterEach(() => { - document.body.removeChild(component); - document.body.innerHTML = ''; - }); - - test('should be created correctly', () => { - expect(component).toBeDefined(); - expect(component.shadowRoot).not.toBeNull(); - }); - - it('should listen for messages', () => { - const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); - eventsManager.listen(); - expect(addEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function)); - }); - - it('should handle messages from allowed origin', () => { - const dispatchEventSpy = jest.spyOn(component, 'dispatchEvent'); - const event = new MessageEvent('message', { - origin: allowedOrigin, - data: { type: 'someType', eventType: 'someEventType' } - }); - - (eventsManager as any).handleMessage(event); - - expect(dispatchEventSpy).toHaveBeenCalledWith(expect.any(CustomEvent)); - expect(dispatchEventSpy.mock.calls[0][0].type).toBe('someEventType'); - }); - - it('should ignore invalid messages', () => { - const dispatchEventSpy = jest.spyOn(component, 'dispatchEvent'); - const event = new MessageEvent('message', { - origin: allowedOrigin, - data: { invalid: 'data' } - }); - - (eventsManager as any).handleMessage(event); - - expect(dispatchEventSpy).not.toHaveBeenCalled(); - }); - - // TODO: Add test for leave room event -}); - -describe('OpenViduMeet Web Component Commands', () => { - let component: OpenViduMeet; - - beforeEach(() => { - component = document.createElement('openvidu-meet') as OpenViduMeet; - document.body.appendChild(component); - }); - - afterEach(() => { - document.body.removeChild(component); - jest.restoreAllMocks(); - document.body.innerHTML = ''; - }); - - test('should be created correctly', () => { - expect(component).toBeDefined(); - expect(component.shadowRoot).not.toBeNull(); - }); - - test('should call sendMessage only once when iframe loads', () => { - const sendMessageSpy = jest.spyOn(component['commandsManager'], 'sendMessage'); - - const iframe = component.shadowRoot?.querySelector('iframe'); - expect(iframe).not.toBeNull(); - - // Simular la primera carga del iframe - iframe?.dispatchEvent(new Event('load')); - - expect(sendMessageSpy).toHaveBeenCalledTimes(1); - expect(sendMessageSpy).toHaveBeenCalledWith({ - action: WebComponentActionType.INITIALIZE, - payload: { domain: window.location.origin } - }); - - // Intentar disparar el evento nuevamente - iframe?.dispatchEvent(new Event('load')); - - // No debería llamarse nuevamente - expect(sendMessageSpy).toHaveBeenCalledTimes(1); - }); - - test('should call commandsManager.sendMessage when leaveRoom is called', () => { - const spy = jest.spyOn(component['commandsManager'], 'sendMessage'); - component.leaveRoom(); - expect(spy).toHaveBeenCalledWith({ action: WebComponentActionType.LEAVE_ROOM }); - }); - - test('should call commandsManager.sendMessage when toggleChat is called', () => { - const spy = jest.spyOn(component['commandsManager'], 'sendMessage'); - component.toggleChat(); - expect(spy).toHaveBeenCalledWith({ action: WebComponentActionType.TOGGLE_CHAT }); - }); -});