Skip to main content

Whaticket: componentes del chat

Casos de prueba unitarios (Karma + Jasmine) de los componentes del chat de src/app/features/whaticket, más los dos specs de rutas. Se documentan 14 specs. El spec de chat-list.component.spec.ts se documenta en otra página. Ningún spec usa xit, fit, xdescribe ni fdescribe, y ninguno está vacío.

src/app/features/whaticket/whaticket-routing.module.spec.ts​

  • SUT: constante routes de whaticket-routing.module.ts.
  • Mocking: sin TestBed; se inspecciona el arreglo de rutas importado y se compara con permissionGuard.

WhaticketRoutingModule routes (permission wiring)​

it()EscenarioResultado esperadoMocks/spies
should gate the lazy "chat" child route on TICKET_READ via permissionGuardDado el arreglo routes, cuando se busca la ruta con path === 'chat', entonces se revisa su configuración.data es igual a { permission: 'TICKET_READ' } y canActivate contiene permissionGuard.Ninguno

Tests en el archivo: 1.

src/app/features/whaticket/chat/chat-routing.module.spec.ts​

  • SUT: constante routes de chat-routing.module.ts.
  • Mocking: sin TestBed; inspección directa de las rutas.

ChatRoutingModule routes (permission wiring)​

it()EscenarioResultado esperadoMocks/spies
should gate the chat page route on TICKET_READ via permissionGuardDado routes, cuando se busca la ruta raíz (path === ''), entonces se revisa su configuración.data es { permission: 'TICKET_READ' } y canActivate contiene permissionGuard.Ninguno

Tests en el archivo: 1.

src/app/features/whaticket/components/chat/chat-conversation/chat-conversation.component.spec.ts​

  • SUT: ChatConversationComponent (conversación de un ticket: composer, respuestas, mensajes programados, favoritos, adjuntos, cabecera).
  • Mocking: TestBed con declarations: [ChatConversationComponent, ShowWithPermissionDirective], FormsModule, TranslateModule.forRoot con TranslateFakeLoader y NO_ERRORS_SCHEMA. Todos los servicios se reemplazan con jasmine.createSpyObj (ChatSocketService con propiedades observables newMessage$, reconnected$, connectionStatus$, etc.; MessageService, FileApiService, StarredMessageService, ChatStateManagerService, Store, ScheduleMessageDialogService, MyPermissionsService...). El Store.select devuelve un BehaviorSubject para selectScheduledMessagesEnabled y of([]) para el resto. component.userId = 'ticket-1' se fija en el beforeEach.

ChatConversationComponent (raíz)​

it()EscenarioResultado esperadoMocks/spies
should createDado el componente instanciado por TestBed, cuando no se hace nada más.El componente existe.Todos los servicios espiados

startReply​

it()EscenarioResultado esperadoMocks/spies
sets replyingTo to the given messageDado replyingTo nulo, cuando se llama startReply(mockReplyTarget).replyingTo pasa a ser el mensaje dado.Ninguno

cancelReply​

it()EscenarioResultado esperadoMocks/spies
clears replyingTo without touching newMessageDado un texto escrito en el composer y una respuesta iniciada, cuando se llama cancelReply().replyingTo es null y newMessage conserva el texto.Ninguno

sendMessage​

it()EscenarioResultado esperadoMocks/spies
sends without parentId when not replyingDado newMessage = 'hola' sin respuesta activa, cuando se llama sendMessage().chatSocket.sendMessage recibe ticketId, content y signature: true (sin parentId); replyingTo queda null.chatSocket.sendMessage
includes parentId when replying and clears replyingTo afterwardDado un mensaje escrito y startReply(mockReplyTarget), cuando se envía.sendMessage incluye parentId: 'msg-001' y replyingTo se limpia.chatSocket.sendMessage
does nothing when the message is emptyDado newMessage = ' ' (solo espacios), cuando se envía.sendMessage no se invoca.chatSocket.sendMessage

scheduledMessagesEnabled$ (verify CRITICAL-1)​

it()EscenarioResultado esperadoMocks/spies
reflects selectScheduledMessagesEnabled from the storeDado el BehaviorSubject del selector, cuando emite false y luego true.scheduledMessagesEnabled$ entrega false y luego true.store.select con BehaviorSubject

rehydration on chat open (verify CRITICAL-3)​

it()EscenarioResultado esperadoMocks/spies
dispatches ScheduledMessageActions.loadByTicket() on ngOnInitDado userId = 'ticket-1', cuando se ejecuta detectChanges() (ngOnInit).store.dispatch recibe loadByTicket({ ticketId: 'ticket-1' }).store.dispatch
dispatches loadByTicket again with the new ticketId when userId changesDado el componente inicializado y dispatch reiniciado, cuando userId cambia a 'ticket-2' y se llama ngOnChanges.Se vuelve a despachar loadByTicket({ ticketId: 'ticket-2' }).store.dispatch (calls.reset())

reconnect rejoin​

it()EscenarioResultado esperadoMocks/spies
rejoins the ticket room on every authenticated transition, not just the firstDado el componente inicializado, cuando connectionStatus$ emite connecting, authenticated, idle y de nuevo authenticated.joinTicket no se llama con connecting; se llama con 'ticket-1' en el primer authenticated (1 vez) y en el segundo (2 veces).connectionStatus$ (Subject), chatSocket.joinTicket
keeps the composer text when the socket is down instead of dropping itDado el socket en estado idle y un texto en el composer, cuando se llama sendMessage().No se envía, newMessage conserva el texto e isConnectionLive es false.connectionStatus$, chatSocket.sendMessage
sends again once the socket is authenticatedDado el socket authenticated y un texto escrito, cuando se envía.sendMessage se invoca y newMessage queda vacío.connectionStatus$, chatSocket.sendMessage
refetches the thread on reconnect, not only on the first loadDado un stateManager que permite cargar, getContactMessages devolviendo página vacía y una conversación con otherParticipantId, cuando reconnected$ emite.getContactMessages se llama una vez más que en la carga inicial.reconnected$, messageService.getContactMessages, stateManagerSpy (canLoadMore, startLoading, finishLoading)
keeps the draft the agent was typing when the socket reconnectsMismo montaje, con newMessage = 'a medio escribir', cuando reconnected$ emite.newMessage sigue siendo 'a medio escribir'.reconnected$, messageService.getContactMessages, stateManagerSpy

composer schedule button DOM visibility (verify W19)​

it()EscenarioResultado esperadoMocks/spies
renders app-schedule-send-button when scheduledMessagesEnabled$ is trueDado el flag en true, cuando se renderiza.Existe un elemento app-schedule-send-button.scheduledMessagesEnabled$
hides app-schedule-send-button from the DOM when scheduledMessagesEnabled$ is falseDado el flag en false, cuando se renderiza.No existe app-schedule-send-button.scheduledMessagesEnabled$

onScheduleSend​

it()EscenarioResultado esperadoMocks/spies
does nothing when the message is empty (same guard as sendMessage)Dado newMessage = ' ', cuando se llama onScheduleSend().No se abre el diálogo ni se envía el mensaje.scheduleDialog.open, chatSocket.sendMessage
opens the dialog and, on confirm, dispatches create — never calling sendMessageDado un texto y un diálogo que resuelve con { body editado, scheduledAt }, cuando se llama y se hace tick() (fakeAsync).Se abre el diálogo; se despacha ScheduledMessageActions.create con messageType: 'chat' y signature: true; sendMessage no se llama; newMessage queda vacío.scheduleDialog.open (promesa), store.dispatch, chatSocket.sendMessage
dispatches create with messageType "note" when isNoteMode is onDado isNoteMode = true y un diálogo que confirma.create con messageType: 'note' y signature: false; isNoteMode vuelve a false.scheduleDialog.open, store.dispatch
includes parentId when replyingDado una respuesta iniciada y un diálogo que confirma.create incluye parentId: 'msg-001'; replyingTo queda null.scheduleDialog.open, store.dispatch
does not dispatch when the dialog is dismissedDado un diálogo que resuelve null (cancelado).No se despacha nada y newMessage conserva 'hola'.scheduleDialog.open (null), store.dispatch

ticketMessageToChat (private mapper)​

it()EscenarioResultado esperadoMocks/spies
carries parentId and parent through onto the resulting ChatMessageDado un TicketMessage con parentId y parent, cuando se invoca el mapper privado ticketMessageToChat.El resultado conserva parentId: 'msg-001' y el objeto parent completo.Ninguno (acceso vía as any)
defaults parentId/parent to null when the source has noneDado un TicketMessage sin parentId ni parent.parentId y parent son null.Ninguno

media download affordance​

it()EscenarioResultado esperadoMocks/spies
renders getMediaType() as "image" and "audio" for the respective mock messagesDado un mensaje media de imagen y uno de audio.getMediaType devuelve 'image' y 'audio'.Ninguno
renders the image preview without a download badge over itDado un mensaje de imagen con url, cuando se renderiza (_messages$.next).Existe el enlace .msg-media-download--image con atributo download y href igual a la url; la vista previa .msg-media-image tiene src igual a la url; no existe .msg-media-download._messages$ (acceso privado)
renders the video preview without inline controlsDado un mensaje de video.Existe .msg-media-video sin atributo controls y con .msg-media-video__play._messages$
renders the custom audio player for an audio messageDado un mensaje de audio.Se renderiza app-audio-player y no hay audio[controls]._messages$
does not render a download link for image/audio messages still pending an uploaded urlDado un mensaje media con media: null (subida pendiente).No existe .msg-media-download._messages$

seedFavoriteMessages (ngOnInit)​

it()EscenarioResultado esperadoMocks/spies
seeds favoriteMessageIds from getIdsByScope, scoped by contactIdDado getIdsByScope devolviendo ['msg-a','msg-b'] y otherParticipantId = 'contact-1', cuando se inicializa.Se llama con { contactId: 'contact-1' } y favoriteMessageIds es el Set de ambos ids.starredMessageService.getIdsByScope
does not call getIdsByScope when there is no contactId yetDado conversation = null.getIdsByScope no se llama.starredMessageService.getIdsByScope
resets favoriteMessageIds to an empty set when the seed request failsDado getIdsByScope que lanza error.favoriteMessageIds.size es 0.getIdsByScope con throwError

onMessageToggleFavorite​

it()EscenarioResultado esperadoMocks/spies
optimistically adds and calls toggle(messageId, "contact")Dado un mensaje no favorito.El id se agrega de inmediato y se llama toggle('msg-1', 'contact').starredMessageService.toggle
optimistically removes when the message was already favoritedDado un mensaje ya favorito.El id se elimina de favoriteMessageIds.starredMessageService.toggle
rolls back the optimistic add and shows an error toast when toggle() failsDado toggle que falla.Se revierte la adición y se llama showNotification con type: 'error'.toggle con throwError, ToastrNotificationService.showNotification
rolls back the optimistic removal when toggle() fails on an already-favorited messageDado un favorito existente y toggle que falla.El id vuelve a estar en favoriteMessageIds.toggle con throwError

jumpToMessage​

it()EscenarioResultado esperadoMocks/spies
scrolls to and highlights the message directly when it is already loadedDado el mensaje ya cargado en _messages$ (fakeAsync), cuando se llama jumpToMessage('msg-loaded').No se llama fetchMessageWindow; highlightedMessageId es el id; el elemento tiene la clase msg-highlighted; tras 2000 ms el resaltado vuelve a null.starredMessageService.fetchMessageWindow, _messages$
fetches the message window and merges it into history when the message is not loadedDado _messages$ vacío y fetchMessageWindow devolviendo el mensaje.Se llama fetchMessageWindow('msg-window', 'contact'), el mensaje se incorpora al historial y queda resaltado.fetchMessageWindow con of(...)
fails soft (no throw, no highlight) when the window fetch errors outDado fetchMessageWindow que falla.No lanza excepción y highlightedMessageId es null.fetchMessageWindow con throwError

isTicketOwner / isLockedByCurrentUser getters (sdd/ticket-lock W1)​

it()EscenarioResultado esperadoMocks/spies
isTicketOwner is true only when conversation.assignedUserId matches the current userDado currentUser._id = 'agent-1', con assignedUserId 'agent-1', 'agent-2' y null.true, false, false.currentUser fijado vía as any
isLockedByCurrentUser is true only when conversation.lockedBy matches the current userDado el mismo usuario, con lockedBy 'agent-1', 'agent-2' y null.true, false, false.currentUser vía as any

ticket-actions-menu lock bindings (sdd/ticket-lock W1)​

it()EscenarioResultado esperadoMocks/spies
binds lockedBy/isOwner/isLockedByMe onto the header app-ticket-actions-menuDado un ticket asignado y bloqueado por agent-1 y currentUser agent-1.El app-ticket-actions-menu de la cabecera recibe lockedBy = 'agent-1', isOwner = true, isLockedByMe = true.currentUser vía as any
reflects an unlocked, non-owned ticket as false/null on the menu bindingsDado un ticket asignado a agent-2, sin bloqueo, y currentUser agent-1.lockedBy nulo, isOwner y isLockedByMe en false.currentUser vía as any

adjuntos con caption​

Helper selectAnImage(): validateImage devuelve null, upload devuelve { _id: 'file-1' } y se invoca onImageSelected.

it()EscenarioResultado esperadoMocks/spies
manda lo escrito en el composer como caption del adjuntoDado newMessage = 'te mando la captura', cuando se selecciona una imagen.sendFile recibe ticketId, fileId: 'file-1' y caption: 'te mando la captura'.fileApi.validateImage, fileApi.upload, chatSocket.sendFile
limpia el composer después de adjuntarMismo montaje.newMessage queda vacío.Igual
no manda caption cuando el composer está vacíoDado newMessage = ' '.sendFile se llama sin la propiedad caption.Igual

channelOption (MC-008)​

it()EscenarioResultado esperadoMocks/spies
returns the CONNECTION_CHANNELS entry matching conversation.channelTypeDado channelType = 'whatsapp-qr'.channelOption es la entrada de CONNECTION_CHANNELS con ese type.Constante real CONNECTION_CHANNELS
returns null when channelType does not match any known channelDado channelType = 'unknown-channel'.null.Ninguno
returns null when conversation is null or undefinedDado conversation null y luego undefined.null en ambos casos.Ninguno

header department badge (MC-008)​

it()EscenarioResultado esperadoMocks/spies
shows the department name and its color when conversation.department is setDado un departamento Soporte con color #ff0000..conv-header__department contiene Soporte y borderColor es rgb(255, 0, 0).TranslateFakeLoader
shows the no-department fallback when conversation.department is absentDado department: null.El badge contiene la clave players.chats.list.noDepartment.TranslateFakeLoader

header assigned operator indicator (MC-008)​

it()EscenarioResultado esperadoMocks/spies
shows the assigned operator name together with the operator icon and the desktop-only labelDado assignedUserName = 'Juan Perez'..conv-header__assigned contiene el nombre, el icono .conv-header__assigned-icon.fa-user y la etiqueta con la clave whaticket.chats.conversation.assignedLabel.TranslateFakeLoader
shows the unassigned fallback with the same operator icon and desktop-only labelDado sin usuario asignado.Existe .conv-header__assigned--empty con la clave whaticket.chats.conversation.unassigned, el mismo icono y la misma etiqueta.TranslateFakeLoader
sets the same assignedTooltip title on both the assigned and the unassigned indicatorDado un usuario asignado y luego sin asignar (re-render).En ambos estados el atributo title es igual a component.assignedTooltip.TranslateFakeLoader

assignedTooltip getter (MC-008)​

it()EscenarioResultado esperadoMocks/spies
resolves the whaticket.chats.conversation.assignedTooltip translation keyDado el getter sin conversación.Devuelve whaticket.chats.conversation.assignedTooltip.TranslateFakeLoader

Tests en el archivo: 54.

src/app/features/whaticket/components/chat/chat-filter-panel/chat-filter-panel.component.spec.ts​

  • SUT: ChatFilterPanelComponent (panel de filtros; el selector de agente único fue retirado).
  • Mocking: TestBed con FormsModule; LabelService como jasmine.createSpyObj (findLabelsPaginated devuelve of({ results: [], total: 0 })) y ContactService como {}.

ChatFilterPanelComponent — legacy single-agent picker removal (sdd/chat-operator-filter D8)​

it()EscenarioResultado esperadoMocks/spies
does not render the "Usuarios asignados" sectionDado el componente renderizado.El texto del DOM no contiene Usuarios asignados.LabelService
does not render any .cfp__avatar item (only the users section used it)Dado el componente renderizado.No existe ningún .cfp__avatar.LabelService
does not expose users/loadingUsers/selectUser/getUserName/open.users anymoreDado la instancia.No tiene users, loadingUsers, selectUser, getUserName ni open.users.Ninguno
never injects TicketService (no /users/get-all call from this panel)Dado la instancia._ticketService es undefined.Ninguno
applyFilters still emits the remaining selection fields untouchedDado selection = { labelId, contactId, status }, cuando se llama applyFilters().apply emite exactamente esa selección, sin assignedUserId.Suscripción a component.apply

Tests en el archivo: 5.

src/app/features/whaticket/components/chat/chat-list/chat-list-item/chat-list-item.component.spec.ts​

  • SUT: ChatListItemComponent (tarjeta de un chat en la lista).
  • Mocking: TestBed con declarations: [ChatListItemComponent], TranslateModule.forRoot con TranslateFakeLoader, Overlay como {} y NO_ERRORS_SCHEMA. El último describe declara además ShowWithPermissionDirective y reemplaza MyPermissionsService con jasmine.createSpyObj.

ChatListItemComponent — presence dot​

it()EscenarioResultado esperadoMocks/spies
defaults isOnline to false when the parent does not bind itDado el componente sin binding de isOnline.isOnline es false.Overlay vacío
renders the presence dot when isOnline is trueDado isOnline = true.Existe .chat-card__presence-dot.Overlay vacío
does not render the presence dot when isOnline is falseDado isOnline = false.No existe .chat-card__presence-dot.Overlay vacío

ChatListItemComponent — padlock indicator​

it()EscenarioResultado esperadoMocks/spies
renders the padlock icon when the conversation carries a lockedByDado lockedBy: 'user-1'.Existe .chat-card__lock-icon.Overlay vacío
does not render the padlock icon when the conversation has no lockedByDado lockedBy: null.No existe el candado.Overlay vacío
does not render the padlock icon when lockedBy is absent entirely (default unlocked)Dado una conversación sin la propiedad lockedBy.No existe el candado.Overlay vacío

ChatListItemComponent — padlock icon styling (sdd/ticket-lock W3)​

it()EscenarioResultado esperadoMocks/spies
does not shrink inside the flex header (stays a fixed-size icon, not squeezed by name/time)Dado un chat bloqueado, cuando se lee el estilo computado del candado.flexShrink es '0'.Overlay vacío
renders with a muted, header-consistent font size instead of the browser default icon sizeMismo montaje.fontSize es '11px'.Overlay vacío

ChatListItemComponent — assigned agent badge​

it()EscenarioResultado esperadoMocks/spies
renders the agent avatar image when the assigned agent has a photoDado agente asignado con assignedUserAvatarUrl.Existe .chat-card__agent-badge-img con src igual a la url.MyPermissionsService.hasPermission devuelve of(true)
renders the agent initials when there is a name but no photoDado nombre Agent One y sin foto.El badge muestra AO; no hay imagen ni .fa-user.hasPermission of(true)
renders the generic icon when there is an assigned agent id but no resolvable nameDado assignedUserId pero sin nombre ni foto.Existe i.fa-user dentro del badge.hasPermission of(true)
does not render the badge when the ticket has no assigned agentDado sin agente asignado.No existe .chat-card__agent-badge.hasPermission of(true)
still renders the badge when the ticket status is closed/resolved (all-states requirement)Dado ticketStatus: 'closed' con agente asignado.El badge se renderiza.hasPermission of(true)
hides the badge when TICKET_VIEW_AGENT_AVATAR is not granted, even with an assigned agentDado hasPermission devolviendo of(false) y agente con foto.Se consulta hasPermission('TICKET_VIEW_AGENT_AVATAR') y no se renderiza el badge.hasPermission of(false)

Tests en el archivo: 14.

src/app/features/whaticket/components/chat/chat-popout/chat-popout.component.spec.ts​

  • SUT: ChatPopoutComponent (ventana emergente del chat).
  • Mocking: TestBed con TranslateModule.forRoot (TranslateFakeLoader) y NO_ERRORS_SCHEMA; sin servicios simulados. Se usa una conversación base mínima.

ChatPopoutComponent​

it()EscenarioResultado esperadoMocks/spies
should createDado el componente con conversation base y detectChanges().El componente existe.Ninguno
defaults isOwner and isLockedByMe to false when the parent does not bind themDado sin bindings del padre.Ambos son false.Ninguno

ticket-actions-menu lock bindings​

it()EscenarioResultado esperadoMocks/spies
binds lockedBy from the conversation and isOwner/isLockedByMe from the parent-computed inputsDado lockedBy: 'agent-1', isOwner = true, isLockedByMe = true.app-ticket-actions-menu se renderiza y recibe lockedBy = 'agent-1', isOwner = true, isLockedByMe = true.NO_ERRORS_SCHEMA
reflects an unlocked, non-owned ticket as false/null on the menu bindingsDado lockedBy: null y flags en false.lockedBy nulo, isOwner y isLockedByMe en false.NO_ERRORS_SCHEMA

toggleMinimize​

it()EscenarioResultado esperadoMocks/spies
flips minimized and emits the new valueDado una suscripción a minimizedChange, cuando se llama toggleMinimize().minimized es true y se emite [true].Suscripción a minimizedChange

Tests en el archivo: 5.

src/app/features/whaticket/components/chat/media-viewer/media-viewer.component.spec.ts​

  • SUT: MediaViewerComponent (visor modal de imagen/video).
  • Mocking: TestBed con NgbActiveModal como jasmine.createSpyObj (close, dismiss) y NO_ERRORS_SCHEMA; url y filename fijados en beforeEach.

MediaViewerComponent​

it()EscenarioResultado esperadoMocks/spies
shows an image without a playerDado kind = 'image'.Existe .media-viewer__image y no hay video.NgbActiveModal
shows a video with its controlsDado kind = 'video'.Existe .media-viewer__video con atributo controls.NgbActiveModal
closes on escapeDado una imagen, cuando se llama onEsc().activeModal.dismiss se invoca.activeModal.dismiss
closes when clicking the backdrop areaCuando se hace clic en .media-viewer.dismiss se invoca.activeModal.dismiss
does not close when clicking the media itselfCuando se hace clic en .media-viewer__image.dismiss no se invoca.activeModal.dismiss
closes when clicking the empty area beside the mediaCuando se hace clic en .media-viewer__stage.dismiss se invoca.activeModal.dismiss
does not close when clicking the video player controlsDado un video, cuando se hace clic en .media-viewer__video.dismiss no se invoca.activeModal.dismiss

Tests en el archivo: 7.

src/app/features/whaticket/components/chat/message-context-menu/message-context-menu.component.spec.ts​

  • SUT: MessageContextMenuComponent (menú contextual de un mensaje, renderizado en un overlay CDK).
  • Mocking: TestBed con OverlayModule real y NO_ERRORS_SCHEMA; mensajes construidos con los helpers buildMessage y buildMedia. El helper openMenuLabels() abre el menú y lee las etiquetas del DOM global; afterEach cierra el menú.

MessageContextMenuComponent​

it()EscenarioResultado esperadoMocks/spies
offers copy for a text messageDado un mensaje de texto, cuando se abre el menú.Etiquetas: Reenviar, Responder, Seleccionar, Destacar, Copiar mensaje.OverlayModule real
offers download instead of copy for a media messageDado un mensaje media con imagen.Etiquetas: Reenviar, Responder, Seleccionar, Destacar, Descargar medios.OverlayModule real
treats audio, video and documents as media tooDado mensajes media de tipo audio, video y document.hasMedia es true en los tres.Ninguno
falls back to copy when the media has no url yetDado messageType: 'media' con media: null.hasMedia es false.Ninguno
emits downloadMedia with the message and closesDado un mensaje media y el menú abierto, cuando se llama onDownloadMedia().downloadMedia emite el mensaje e isOpen queda false.Suscripción a downloadMedia
shows the delete option only on own messagesDado isOwn = false.Las etiquetas no incluyen Borrar.OverlayModule real
reflects the favorite state in the labelDado isFavorite = true.Las etiquetas incluyen Quitar destacado.OverlayModule real

Tests en el archivo: 7.

src/app/features/whaticket/components/chat/postpone-dialog/postpone-dialog.component.spec.ts​

  • SUT: PostponeDialogComponent (diálogo para posponer, standalone).
  • Mocking: TestBed importando el componente y TranslateModule.forRoot(); NgbActiveModal como jasmine.createSpyObj (close, dismiss). Helper futureDateStruct(hours).

PostponeDialogComponent​

it()EscenarioResultado esperadoMocks/spies
treats a past date/time as invalidDado fecha y hora una hora en el pasado.isValidFutureSelection() es false.NgbActiveModal
treats a future date/time as validDado fecha y hora una hora en el futuro.isValidFutureSelection() es true.NgbActiveModal
does not close the modal when confirming with a past date/timeDado fecha pasada, cuando se llama onConfirm().activeModal.close no se invoca.activeModal.close
resolves the combined Date when confirming with a valid future date/timeDado fecha y hora dos horas en el futuro.close se llama una vez con un Date cuyo año y hora coinciden con la selección.activeModal.close
dismisses the modal on cancelCuando se llama onCancel().dismiss se llama con null.activeModal.dismiss

Tests en el archivo: 5.

src/app/features/whaticket/components/chat/schedule-message-dialog/schedule-message-dialog.component.spec.ts​

  • SUT: ScheduleMessageDialogComponent (diálogo para programar/editar un mensaje, standalone).
  • Mocking: TestBed con TranslateModule.forRoot() y NgbActiveModal como jasmine.createSpyObj (close, dismiss).

ScheduleMessageDialogComponent​

it()EscenarioResultado esperadoMocks/spies
pre-fills the body from initialBodyDado initialBody = 'hello there' y ngOnInit().body es 'hello there'.NgbActiveModal
pre-fills scheduledAt from initialScheduledAt and allows confirming without touching the picker (verify CRITICAL-4)Dado initialScheduledAt 48 h adelante, cuerpo editado a 'typo fixed' y sin tocar el selector, cuando se confirma.close recibe { body: 'typo fixed', scheduledAt: existing }.activeModal.close
does not confirm with a past date/timeDado cuerpo válido y fecha pasada marcada como inválida.close no se invoca.activeModal.close
does not confirm with an empty bodyDado cuerpo ' ' y fecha futura válida.close no se invoca.activeModal.close
confirms with { body, scheduledAt } when body and date are validDado cuerpo y fecha futura válidos.close recibe { body: 'hello there', scheduledAt: future }.activeModal.close
dismisses the modal on cancelCuando se llama onCancel().dismiss se llama con null.activeModal.dismiss

Tests en el archivo: 6.

src/app/features/whaticket/components/chat/schedule-send-button/schedule-send-button.component.spec.ts​

  • SUT: ScheduleSendButtonComponent (botón de envío programado, standalone).
  • Mocking: TestBed con TranslateModule.forRoot(); jasmine.createSpy('schedule') suscrito al EventEmitter.

ScheduleSendButtonComponent​

it()EscenarioResultado esperadoMocks/spies
emits schedule on click when not disabledDado el botón habilitado, cuando se llama onClick().schedule emite una vez.jasmine.createSpy('schedule')
does not emit schedule on click when disabledDado disabled = true.schedule no emite.jasmine.createSpy('schedule')

Tests en el archivo: 2.

src/app/features/whaticket/components/chat/scheduled-message-strip/scheduled-message-strip.component.spec.ts​

  • SUT: ScheduledMessageStripComponent (franja de mensajes programados pendientes, standalone).
  • Mocking: TestBed con TranslateModule.forRoot(); Store como jasmine.createSpyObj (select devuelve of([makeItem()]), dispatch) y ScheduleMessageDialogService con open espiado. ticketId = 'tk1'.

ScheduledMessageStripComponent​

it()EscenarioResultado esperadoMocks/spies
exposes the pending items for the given ticket via items$ (a sibling stream to messages$, never merged into it)Dado el store con un item sm1.items$ entrega 1 elemento con _id = 'sm1'.store.select
dispatches cancel with the id and item when the agent cancels a pending rowCuando se llama onCancel(item).Se despacha ScheduledMessageActions.cancel({ id, item }).store.dispatch

onEdit​

it()EscenarioResultado esperadoMocks/spies
opens the dialog pre-loaded with the existing scheduledAtDado un item con scheduledAt y diálogo que resuelve null.open se llama con initialBody, isNote: false e initialScheduledAt como Date.scheduleDialog.open
omits scheduledAt from the update dispatch when the agent only changed the bodyDado un diálogo que devuelve nuevo cuerpo y la misma fecha.Se despacha update({ id, body: 'texto corregido' }) sin scheduledAt.scheduleDialog.open, store.dispatch
includes scheduledAt in the update dispatch when the agent actually changed itDado un diálogo que devuelve otra fecha.update incluye scheduledAt en ISO.scheduleDialog.open, store.dispatch
does not dispatch when the dialog is dismissedDado un diálogo que resuelve null.dispatch no se invoca.scheduleDialog.open, store.dispatch

Tests en el archivo: 6.

src/app/features/whaticket/components/chat/ticket-actions-menu/ticket-actions-menu.component.spec.ts​

  • SUT: TicketActionsMenuComponent (acciones bloquear/desbloquear; solo se prueban getters y select(), no la plantilla en overlay).
  • Mocking: TestBed con Overlay como {} y NO_ERRORS_SCHEMA; spyOn(component.action, 'emit').

canLockTicket​

it()EscenarioResultado esperadoMocks/spies
is true when the ticket is unlocked and the requester owns itlockedBy = null, isOwner = true.true.Ninguno
is false when the ticket is already locked (by anyone)lockedBy = 'user-1', isOwner = true.false.Ninguno
is false when the requester does not own the ticketlockedBy = null, isOwner = false.false.Ninguno

canUnlockTicket​

it()EscenarioResultado esperadoMocks/spies
is true when the ticket is locked by the requester themselveslockedBy = 'user-1', isLockedByMe = true.true.Ninguno
is false when the ticket is unlockedlockedBy = null, isLockedByMe = false.false.Ninguno
is false when the ticket is locked by someone else (not authorized here — force-unlock lives in the admin view)lockedBy = 'user-2', isLockedByMe = false.false.Ninguno

select()​

it()EscenarioResultado esperadoMocks/spies
emits "lock" when selecting the lock actionCuando se llama select('lock').action.emit se llama con 'lock'.spyOn(component.action, 'emit')
emits "unlock" when selecting the unlock actionCuando se llama select('unlock').action.emit se llama con 'unlock'.spyOn(component.action, 'emit')

Tests en el archivo: 8.

src/app/features/whaticket/components/chat/transfer-panel/transfer-panel.component.spec.ts​

  • SUT: TransferPanelComponent (formulario de transferencia a agente y departamento).
  • Mocking: TestBed con ReactiveFormsModule, TranslateModule.forRoot() y NO_ERRORS_SCHEMA. TicketService y DepartmentService con funciones que devuelven of(...); los tokens 'ngAgents' y 'ngDepartments' usan una fábrica searchServiceMock() con espías (resetNgSelect, extendFilter, triggerFetchData, getData...). overrideComponent deja la plantilla vacía a propósito: se prueba la lógica del formulario, no el markup.

TransferPanelComponent — department scoped to the selected agent​

it()EscenarioResultado esperadoMocks/spies
keeps the department control disabled until an agent is chosenDado el formulario recién creado.departmentId está deshabilitado y triggerFetchData no se ha llamado.ngDepartments.triggerFetchData
loads the departments filtered by the selected agent membershipCuando agentId se fija en 'agent-1'.departmentId se habilita; se llaman resetNgSelect, extendFilter({ userId: 'agent-1' }) y triggerFetchData.ngDepartments
clears the chosen department and refetches when the agent changesDado agente y departamento elegidos, cuando el agente cambia a 'agent-2'.El departamento vuelve a null y se llama extendFilter({ userId: 'agent-2' }).ngDepartments.extendFilter
blocks the transfer until both the agent and the department are chosenSin agente, con solo agente y con ambos.canTransfer es false, false y true.Ninguno
does not emit the selection while the department is missingDado solo agente elegido, cuando se llama onTransfer().select.emit no se llama.spyOn(component.select, 'emit')
emits the chosen department so the card can be repainted without a refetchDado getData con el departamento Soporte (#0d6efd), un componente nuevo suscrito manualmente a departments$, y agente y departamento elegidos.select.emit recibe departmentId: 'dept-1' y department: { _id: 'dept-1', name: 'Soporte', color: '#0d6efd' }.ngDepartments.getData, spyOn(fresh.select, 'emit')
disables and clears the department when the agent is clearedDado agente y departamento elegidos, cuando agentId pasa a null.departmentId queda deshabilitado y con valor null.Ninguno

Tests en el archivo: 7.

Total de la página​

14 specs, 128 tests (it()): rutas whaticket 1, rutas chat 1, chat-conversation 54, chat-filter-panel 5, chat-list-item 14, chat-popout 5, media-viewer 7, message-context-menu 7, postpone-dialog 5, schedule-message-dialog 6, schedule-send-button 2, scheduled-message-strip 6, ticket-actions-menu 8, transfer-panel 7.