Casos de prueba: Chat interno y mensajes programados
Specs de src/app/features/internal-chat/** (5 archivos) y src/app/features/scheduled-messages/** (3 archivos). Todos usan Karma + Jasmine. La mayoría instancia el componente directamente con new y dependencias simuladas; los de scheduled-messages filtro y tabla usan TestBed. No hay xit, fdescribe ni specs vacíos.
Chat interno
src/app/features/internal-chat/components/conversation-thread/conversation-thread.component.spec.ts
- SUT:
ConversationThreadComponent.
- Mocking: instancia directa con
new ConversationThreadComponent(...); la mayoría de dependencias son {} as any. En el primer bloque se usan un fakeTranslate (con instant que interpola {{param}}) y un fakeStateManager (getInitialState). En el segundo bloque, jasmine.createSpyObj para StarredMessageService (toggle, getIdsByScope, fetchMessageWindow) y ToastrNotificationService (showNotification), con of/throwError de RxJS.
Bloque ConversationThreadComponent — system messages > getSystemMessageText
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
renders a grouped member_added system message as one line naming both targets | Dado un mensaje de sistema member_added con actor Ana y destinos Beto y Caro, cuando se llama getSystemMessageText, entonces se agrupan los destinos. | Devuelve Ana added Beto and Caro. | fakeTranslate.instant con diccionario de traducciones. |
falls back to a generic message for an unrecognized systemEvent.type, not blank | Dado un systemEvent.type desconocido (renamed), cuando se llama getSystemMessageText, entonces se usa el texto genérico (camino de borde). | Devuelve Ana made a change to the group, no vacío. | fakeTranslate.instant. |
Bloque ConversationThreadComponent — system messages > shouldRenderMessage
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
renders known content types (text, file, system) | Dado un mensaje con contentType text, file o system, cuando se llama shouldRenderMessage, entonces se acepta. | Los tres casos devuelven true. | Ninguno (dependencias vacías). |
skips an unrecognized contentType instead of crashing (legacy/forward compat) | Dado un contentType desconocido (poll), cuando se llama shouldRenderMessage, entonces se omite sin lanzar error. | Devuelve false. | Ninguno. |
Total del bloque: 4 tests.
Bloque ConversationThreadComponent — starred messages (WU4) > isFavorite / onToggleFavorite
beforeEach fija component.activeConversationId = 'c1'.
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
optimistically stars a message and calls toggle with messageType internal | Dado un mensaje no marcado y toggle que responde { starred: true }, cuando se llama onToggleFavorite, entonces se marca de forma optimista. | isFavorite('m1') es true y toggle se llamó con ('m1', 'internal'). | starredMessageService.toggle devuelve of({ starred: true }). |
optimistically unstars an already-starred message | Dado m1 ya en favoriteMessageIds y toggle que responde { starred: false }, cuando se llama onToggleFavorite, entonces se desmarca. | isFavorite('m1') es false. | toggle devuelve of({ starred: false }). |
rolls back and shows a toast when the toggle request fails | Dado que toggle falla, cuando se llama onToggleFavorite, entonces se revierte el cambio optimista y se notifica (camino de fallo). | isFavorite('m1') es false y toastr.showNotification fue llamado. | toggle devuelve throwError; espía toastr.showNotification. |
Bloque ConversationThreadComponent — starred messages (WU4) > jumpToMessage
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
fetches a message window scoped to the active conversation when not already rendered | Dado que m2 no está renderizado y hay conversación activa c1, cuando se llama jumpToMessage('m2'), entonces se pide la ventana de mensajes. | fetchMessageWindow llamado con ('m2', 'internal', 'c1'). | fetchMessageWindow devuelve of({ results, anchorId: 'm2' }). |
is a no-op when there is no active conversation | Dado activeConversationId = null, cuando se llama jumpToMessage('m2'), entonces no hace nada. | fetchMessageWindow no se llama. | Espía fetchMessageWindow. |
fails soft (no throw) when the window fetch errors | Dado que fetchMessageWindow emite error, cuando se llama jumpToMessage('m2'), entonces el error se absorbe (camino de fallo). | No lanza excepción. | fetchMessageWindow devuelve throwError. |
Total del bloque: 6 tests. Total del archivo: 10 tests.
src/app/features/internal-chat/components/starred-messages-panel/starred-messages-panel.component.spec.ts
- SUT:
StarredMessagesPanelComponent.
- Mocking: instancia directa con
new; el primer argumento es un objeto con dismiss/close vacíos (modal activo), el segundo jasmine.createSpyObj('StarredMessageService', ['findByInternalConversation']) y el tercero un formateador de fechas con getFullDate. Helper listItem(overrides) construye StarredMessageListItem. conversationId = 'c1' en el beforeEach.
ngOnInit / loadStarred
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
loads the first page of starred messages for the conversation | Dado un servicio que devuelve una página con 1 resultado, cuando se llama ngOnInit, entonces se carga la primera página. | Servicio llamado con ('c1', 1, 20), items.length es 1 e isLoading es false. | findByInternalConversation devuelve of({ results, meta }). |
resets to an empty list when the request fails | Dado que la petición falla, cuando se llama ngOnInit, entonces la lista queda vacía (camino de fallo). | items es [] e isLoading es false. | findByInternalConversation devuelve throwError. |
loadMore
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
requests the next page and appends results | Dado items con star-1 y meta en página 1 de 2, cuando se llama loadMore, entonces se pide la página 2 y se anexa. | Servicio llamado con ('c1', 2, 20); ids ['star-1', 'star-2']. | findByInternalConversation devuelve of con star-2. |
no-ops when already on the last page | Dado meta en la última página (2 de 2), cuando se llama loadMore, entonces no hace nada (borde). | El servicio no se llama. | Espía findByInternalConversation. |
getPreview
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
renders the hydrated file filename when a file is present (WU5) | Dado un ítem con archivo audio.ogg, cuando se llama getPreview, entonces se muestra el nombre del archivo. | Devuelve audio.ogg. | Ninguno. |
falls back to "[archivo]" when the file has no filename | Dado un archivo sin filename, cuando se llama getPreview, entonces se usa el texto por defecto (borde). | Devuelve [archivo]. | Ninguno. |
renders the raw body when there is no attached file | Dado un ítem sin archivo y con body: 'hola mundo', cuando se llama getPreview, entonces se muestra el cuerpo. | Devuelve hola mundo. | Ninguno. |
returns an empty string for an orphaned item (message is null) | Dado un ítem huérfano con message: null, cuando se llama getPreview, entonces no falla (borde). | Devuelve ''. | Ninguno. |
getSenderName
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
returns firstname + lastName when both are present | Dado un remitente con nombre y apellido, cuando se llama getSenderName, entonces se concatenan. | Devuelve Grace Hopper. | Ninguno. |
falls back to username when firstname/lastName are missing | Dado un remitente solo con username, cuando se llama getSenderName, entonces se usa el usuario. | Devuelve grace. | Ninguno. |
returns an empty string when sender is undefined (internal system message) | Dado sender: undefined, cuando se llama getSenderName, entonces devuelve vacío (borde). | Devuelve ''. | Ninguno. |
hasFile / isFileImage / getFileIconClass
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
reports no file when file is null | Dado file: null, cuando se llama hasFile, entonces indica que no hay archivo. | false. | Ninguno. |
reports a file and detects an image mimetype | Dado un archivo image/jpeg, cuando se consultan hasFile, isFileImage y getFileIconClass, entonces se detecta imagen. | true, true y fa-image. | Ninguno. |
resolves the microphone icon for an audio file | Dado un archivo audio/ogg, cuando se consultan isFileImage y getFileIconClass, entonces no es imagen y el icono es de micrófono. | false y fa-microphone. | Ninguno. |
onItemClick
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
emits jumpToMessage with the item messageId | Dado un suscriptor a jumpToMessage, cuando se llama onItemClick con messageId: 'target-msg', entonces se emite el id. | Emitido ['target-msg']. | Suscripción manual al EventEmitter. |
Total del archivo: 15 tests.
src/app/features/internal-chat/conversations/conversations-routing.module.spec.ts
- SUT: arreglo
routes de conversations-routing.module.
- Mocking: ninguno; inspecciona la configuración de rutas e importa
permissionGuard.
ConversationsRoutingModule routes (permission wiring)
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should gate the conversations page route on INTERNAL_CHAT_READ via permissionGuard | Dada la ruta con path === '', cuando se inspeccionan data y canActivate, entonces exige el permiso. | data igual a { permission: 'INTERNAL_CHAT_READ' } y canActivate contiene permissionGuard. | Ninguno. |
Total del archivo: 1 test.
src/app/features/internal-chat/internal-chat-routing.module.spec.ts
- SUT: arreglo
routes de internal-chat-routing.module.
- Mocking: ninguno; inspección de configuración.
InternalChatRoutingModule routes (permission wiring)
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should gate the lazy "conversations" child route on INTERNAL_CHAT_READ via permissionGuard | Dada la ruta hija con path === 'conversations', cuando se inspeccionan data y canActivate, entonces exige el permiso. | data igual a { permission: 'INTERNAL_CHAT_READ' } y canActivate contiene permissionGuard. | Ninguno. |
Total del archivo: 1 test.
src/app/features/internal-chat/pages/internal-chat-page/internal-chat-page.component.spec.ts
- SUT:
InternalChatPageComponent (enlace profundo highlightMessageId).
- Mocking: instancia directa con
new. store con select (devuelve of(null)) y dispatch como espías; route con queryParams: of(...); socket con connectionStatus$: of('authenticated') y connect; globalService con profile: of(null); el resto {} as any. Se inyecta _threadRef simulado mediante (component as any).
InternalChatPageComponent — starred-message deep link (WU4)
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
dispatches setActiveConversation when a conversationId query param is present | Dado el query param conversationId: 'c1', cuando se llama ngOnInit, entonces se activa la conversación. | dispatch llamado con un objeto que contiene conversationId: 'c1'. | store.dispatch espía. |
delegates the jump to the mounted thread once its activeConversationId matches | Dados conversationId y highlightMessageId y un hilo ya en c1, cuando se llama ngOnInit, entonces delega el salto. | jumpToMessage llamado con 'm1'. | _threadRef simulado con espía jumpToMessage. |
retries (bounded) until the thread has switched to the target conversation | Dado un hilo con activeConversationId: null, cuando se llama ngOnInit y luego el hilo pasa a c1, entonces se reintenta (reintento acotado, espera 150 ms con done). | Al inicio jumpToMessage no se llama; tras 150 ms se llama con 'm1'. | _threadRef mutable, setTimeout, espía jumpToMessage. |
does nothing extra when only conversationId is present (no highlightMessageId) | Dado solo conversationId y un hilo en c1, cuando se llama ngOnInit, entonces no hay salto (borde). | jumpToMessage no se llama. | _threadRef simulado con espía. |
Total del archivo: 4 tests.
Mensajes programados
src/app/features/scheduled-messages/components/scheduled-messages-filter/scheduled-messages-filter.component.spec.ts
- SUT:
ScheduledMessagesFilterComponent (standalone).
- Mocking:
TestBed con imports: [ScheduledMessagesFilterComponent, TranslateModule.forRoot()], createComponent y detectChanges; espía jasmine.createSpy('filterChange') suscrito al output.
ScheduledMessagesFilterComponent
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
emits filterChange with the selected status | Dado un suscriptor a filterChange, cuando se llama onStatusChange('pending'), entonces se emite el estado. | Espía llamado con { status: 'pending' }. | Espía filterChange. |
emits filterChange with no status when "all" is selected | Dado un suscriptor, cuando se llama onStatusChange('') (opción "todos"), entonces el estado queda indefinido (borde). | Espía llamado con { status: undefined }. | Espía filterChange. |
Total del archivo: 2 tests.
src/app/features/scheduled-messages/components/scheduled-messages-table/scheduled-messages-table.component.spec.ts
- SUT:
ScheduledMessagesTableComponent (standalone).
- Mocking:
TestBed con TranslateModule.forRoot(); helper makeItem(overrides) construye un ScheduledMessage con status: 'pending'; espía sobre el output cancel.
ScheduledMessagesTableComponent
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
exposes isCancellable as true only for pending items | Dados ítems con estado pending, sent y cancelled, cuando se llama isCancellable, entonces solo el pendiente es cancelable. | true para pending; false para sent y cancelled. | Ninguno. |
emits cancel with the item when onCancelClick is called | Dado un suscriptor a cancel, cuando se llama onCancelClick(item), entonces se emite el ítem. | Espía llamado con item. | Espía cancel. |
Total del archivo: 2 tests.
src/app/features/scheduled-messages/pages/scheduled-messages-page/scheduled-messages-page.component.spec.ts
- SUT:
ScheduledMessagesPageComponent.
- Mocking:
Store de NgRx simulado con jasmine.createSpyObj('Store', ['select', 'dispatch']), select devuelve of([makeItem()]); se registra como provider en TestBed, pero el componente se instancia directamente con new ScheduledMessagesPageComponent(store).
ScheduledMessagesPageComponent
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
dispatches loadMine on init | Dado el componente creado, cuando se llama ngOnInit, entonces se pide la carga propia. | dispatch llamado con ScheduledMessageActions.loadMine({}). | store.dispatch espía. |
dispatches cancel with the id and item when onCancel is called | Dado un ítem, cuando se llama onCancel(item), entonces se despacha la cancelación. | dispatch llamado con ScheduledMessageActions.cancel({ id: item._id, item }). | store.dispatch espía. |
dispatches loadMine with the filter status when onFilterChange is called | Dado un filtro { status: 'sent' }, cuando se llama onFilterChange, entonces se recarga con ese estado. | dispatch llamado con ScheduledMessageActions.loadMine({ status: 'sent' }). | store.dispatch espía. |
Total del archivo: 3 tests.
Total de la página
8 specs, 36 tests (it()): chat interno 31 (10 + 15 + 1 + 1 + 4) y mensajes programados 7 (2 + 2 + 3).