Skip to main content

Estados NgRx del core

Casos de prueba de los 14 specs bajo src/app/core/states/**: acciones, reducers, efectos y selectores de los slices auth, internal-chat, language, scheduled-message, settings y ticket.

Patrones de mocking usados:

  • Reducers, acciones y selectores: funciones puras, se invocan directamente (reducer(estado, accion)) sin TestBed ni espías, salvo los dos specs de language que espían localStorage.
  • Efectos: TestBed.configureTestingModule con provideMockActions(() => actions$) y servicios reemplazados por jasmine.createSpyObj. Cada caso asigna actions$ = of(accion) y se suscribe al efecto con done.

No hay xit, fit, xdescribe ni fdescribe en estos specs, y ningún spec está vacío.

auth​

src/app/core/states/auth/auth.actions.spec.ts​

  • SUT: UserActions (auth.actions.ts).
  • Mocking: sin TestBed; se crean los action creators y se verifica type y payload.

Session & Authentication​

it()EscenarioResultado esperadoMocks/spies
should create loadUserSession actionDado el action creator, cuando se invoca loadUserSession()type es [User] Load SessionNinguno
should create userAuthenticationRequest with request payloadDado un request con email y password, cuando se invoca userAuthenticationRequest({ request })type es [User] Authentication Request y action.request igual al requestNinguno
should create userAuthenticationSuccess with user payloadDado un user con _id, email y token, cuando se invoca userAuthenticationSuccess({ user })type es [User] Authentication Success y action.user igual al usuarioNinguno
should create userAuthenticationFailure with message payloadDado el mensaje Invalid credentials, cuando se invoca userAuthenticationFailure({ message })type es [User] Authentication Failure y action.message es el mensajeNinguno

Logout​

it()EscenarioResultado esperadoMocks/spies
should create userLogout actionCuando se invoca userLogout()type es [User] LogoutNinguno
should create completeUserLogout actionCuando se invoca completeUserLogout()type es [User] Complete LogoutNinguno

Tests en este spec: 6.

src/app/core/states/auth/auth.effects.spec.ts​

  • SUT: AuthEffects (loadUserSession$, loginUser$, logoutUser$).
  • Mocking: TestBed con provideMockActions. Providers: AuthService (login, logout), UserService (getMeUser), StorageService con { secureStorage } (spy de getItem, setItem, removeItem), Router (navigate), TranslateService (instant devuelve la clave) y Store falso (select, dispatch).

loadUserSession$​

it()EscenarioResultado esperadoMocks/spies
dispatches failure when there is no stored sessionDado que secureStorage.getItem devuelve null, cuando llega loadUserSessionEmite userAuthenticationFailure con auth.errorMessages.noActiveSessionsecureStorage.getItem
dispatches success and merges token into user when session and API succeedDado una sesión guardada con token y getMeUser que responde con el usuario, cuando llega loadUserSessionEmite [User] Authentication Success con user.token igual al token guardado y user.email del usuario de la APIsecureStorage.getItem, userService.getMeUser
dispatches failure and clears session when getMeUser returns 401Dado una sesión con token expirado y getMeUser que falla con Unauthorized, cuando llega loadUserSessionEmite userAuthenticationFailure con Unauthorized y se llama secureStorage.removeItem(USER_SESSION)secureStorage.getItem, secureStorage.removeItem, userService.getMeUser

loginUser$​

it()EscenarioResultado esperadoMocks/spies
stores session, fetches user, navigates to chat and dispatches successDado login que responde con token y getMeUser con el usuario, cuando llega userAuthenticationRequestEmite [User] Authentication Success con el token, se llama secureStorage.setItem y router.navigate con ['/whaticket/chat']authService.login, userService.getMeUser, secureStorage.setItem, router.navigate
dispatches failure when login API call failsDado login que falla con Invalid credentials, cuando llega userAuthenticationRequestEmite userAuthenticationFailure con Invalid credentialsauthService.login
dispatches failure when getMeUser fails after a successful loginDado login correcto pero getMeUser que falla con Server errorEmite userAuthenticationFailure con Server errorauthService.login, userService.getMeUser
falls back to error.message when error.error.message is absentDado login que falla con un error sin error.error.message (solo message: 'Network error')Emite userAuthenticationFailure con Network errorauthService.login

logoutUser$​

it()EscenarioResultado esperadoMocks/spies
calls authService.logout and dispatches completeUserLogoutCuando llega userLogoutSe llama authService.logout y se emite [User] Complete LogoutauthService.logout

Tests en este spec: 8.

src/app/core/states/auth/auth.reducers.spec.ts​

  • SUT: authReducer e initialUserState.
  • Mocking: función pura, sin spies.
it()EscenarioResultado esperadoMocks/spies
should return the initial state for an unknown actionDado estado undefined y una acción desconocida @@INITDevuelve initialUserStateNinguno

loadUserSession​

it()EscenarioResultado esperadoMocks/spies
should set loading to trueDado el estado inicial, cuando llega loadUserSessionloading es trueNinguno

userAuthenticationRequest​

it()EscenarioResultado esperadoMocks/spies
should set loading to trueDado el estado inicial, cuando llega userAuthenticationRequestloading es trueNinguno

userAuthenticationSuccess​

it()EscenarioResultado esperadoMocks/spies
should store the user and set loading to falseDado el estado inicial, cuando llega userAuthenticationSuccess con un usuariouser igual al usuario y loading es falseNinguno
should overwrite a previously stored userDado un estado con usuario ya guardado, cuando llega un segundo success con otro emailuser.email es new@test.comNinguno

userAuthenticationFailure​

it()EscenarioResultado esperadoMocks/spies
should set loading to false and keep user as nullDado un estado loading: true sin usuario, cuando llega userAuthenticationFailureloading es false y user es nullNinguno
should not remove an existing user on failureDado un estado con usuario logueado, cuando llega userAuthenticationFailureEl usuario se conserva (el logout se maneja aparte)Ninguno

userLogout​

it()EscenarioResultado esperadoMocks/spies
should set loading to true while logging outDado el estado inicial, cuando llega userLogoutloading es trueNinguno

completeUserLogout​

it()EscenarioResultado esperadoMocks/spies
should reset state to initial valuesDado un estado con usuario logueado, cuando llega completeUserLogoutuser es null y loading es falseNinguno

Tests en este spec: 9.

src/app/core/states/auth/auth.selectors.spec.ts​

  • SUT: selectAuth, selectUser, selectLoading, selectErrorMessage, selectUserId, selectScheduledMessagesEnabled.
  • Mocking: se construye un AppState parcial con el helper buildState(overrides); sin spies.

selectAuth​

it()EscenarioResultado esperadoMocks/spies
should select the auth slice of stateDado un estado con slice authDevuelve state.authNinguno

selectUser​

it()EscenarioResultado esperadoMocks/spies
should return the current userDado un estado con usuarioDevuelve el usuarioNinguno
should return null when no user is logged inDado user: nullDevuelve nullNinguno

selectLoading​

it()EscenarioResultado esperadoMocks/spies
should return false when not loadingDado loading: falseDevuelve falseNinguno
should return true when loadingDado loading: trueDevuelve trueNinguno

selectErrorMessage​

it()EscenarioResultado esperadoMocks/spies
should return the current messageDado message: 'Some error'Devuelve Some errorNinguno
should return null when message is nullDado message: nullDevuelve nullNinguno

selectUserId​

it()EscenarioResultado esperadoMocks/spies
should return the user _idDado un usuario con _id user-1Devuelve user-1Ninguno
should return undefined when user is nullDado user: nullDevuelve undefinedNinguno

selectScheduledMessagesEnabled​

El flag viene de featureFlags del estado auth/profile, no del slice settings (restringido a administradores).

it()EscenarioResultado esperadoMocks/spies
defaults to false (fail-closed) when there is no user yetDado user: nullDevuelve falseNinguno
defaults to false when featureFlags is missing from the user payloadDado un usuario sin featureFlagsDevuelve falseNinguno
reflects featureFlags.scheduledMessagesEnabled when presentDado un usuario con featureFlags.scheduledMessagesEnabled en true y luego en falseDevuelve true y luego false respectivamenteNinguno

Tests en este spec: 12.

internal-chat​

src/app/core/states/internal-chat/internal-chat.reducers.spec.ts​

  • SUT: internalChatReducer e initialInternalChatState.
  • Mocking: función pura, sin spies; payloads de socket construidos en línea.

internalChatReducer — newMessageFromSocket / system message mapping​

it()EscenarioResultado esperadoMocks/spies
maps systemEvent/actorName/targetNames onto the appended messageDado la conversación activa conv-1 y un mensaje de socket contentType: 'system' con systemEvent, actorName y targetNamesEl mensaje añadido conserva systemEvent (member_added), actorName Ana y targetNames ['Beto']Ninguno
leaves systemEvent/actorName/targetNames undefined for a regular text messageDado un mensaje de socket contentType: 'text' sin campos de sistemaLos tres campos quedan undefinedNinguno

internalChatReducer — conversationUpdatedFromSocket / unread bump guard (system flag)​

it()EscenarioResultado esperadoMocks/spies
bumps unread when the conversation is inactive and the payload is a normal messageDado la conversación conv-1 inactiva (activa conv-2) y un payload sin systemunreadCount pasa a 1Ninguno
does NOT bump unread when the conversation is inactive but the payload carries system: trueDado la misma conversación inactiva y un payload con system: trueunreadCount sigue en 0 y lastMessagePreview se actualiza a X added YNinguno

Tests en este spec: 4.

language​

src/app/core/states/language/language.effects.spec.ts​

  • SUT: LanguageEffects (loadPreviewLanguage, tenantAuthenticatedLanguage$).
  • Mocking: TestBed con provideMockActions y TranslateService espía (use, setDefaultLang); el helper setup(action) configura el módulo en cada caso.

loadPreviewLanguage​

it()EscenarioResultado esperadoMocks/spies
defaults to Spanish without reading localStorageCuando llega loadPreviewLanguageEmite loadPreviewLanguageSuccess({ language: 'es' }) y no se lee la clave Language de localStoragespyOn(localStorage, 'getItem') con callThrough

tenantAuthenticatedLanguage$​

it()EscenarioResultado esperadoMocks/spies
applies the tenant language coming from get-meDado userAuthenticationSuccess con tenantPreferences.language enEmite change({ language: 'en' })TranslateService espía
falls back to Spanish when the tenant language is missing or unsupportedDado userAuthenticationSuccess con idioma fr (no soportado)Emite change({ language: 'es' })TranslateService espía

Tests en este spec: 3.

src/app/core/states/language/language.reducers.spec.ts​

  • SUT: languageReducer.
  • Mocking: función pura; espía sobre localStorage.setItem.
it()EscenarioResultado esperadoMocks/spies
stores the language without persisting it to localStorageDado { code: 'es' }, cuando llega change({ language: 'en' })code es en y localStorage.setItem no se llamaspyOn(localStorage, 'setItem')
stores the language coming from the bootstrap effectDado { code: 'es' }, cuando llega loadPreviewLanguageSuccess({ language: 'en' })code es enNinguno

Tests en este spec: 2.

scheduled-message​

src/app/core/states/scheduled-message/scheduled-message.effects.spec.ts​

  • SUT: ScheduledMessageEffects (loadMine$, loadByTicket$, create$, update$, cancel$).
  • Mocking: TestBed con provideMockActions y ScheduledMessageService espía (create, getMine, getByTicket, update, cancel); helper makeItem para el mensaje programado.

loadMine$​

it()EscenarioResultado esperadoMocks/spies
dispatches loadMineSuccess with the resultsDado getMine que devuelve un resultado paginado, cuando llega loadMine({ status: 'pending' })getMine se llama con { status: 'pending' } y se emite loadMineSuccess({ items: [item] })service.getMine
dispatches loadMineFailure on errorDado getMine que falla con boomEmite una acción de tipo [ScheduledMessage] Load Mine Failureservice.getMine

loadByTicket$​

it()EscenarioResultado esperadoMocks/spies
dispatches loadByTicketSuccess with the ticketId and resultsDado getByTicket que devuelve [item], cuando llega loadByTicket({ ticketId: 'tk1' })getByTicket se llama con tk1 y se emite loadByTicketSuccess con ticketId e itemsservice.getByTicket
dispatches loadByTicketFailure on errorDado getByTicket que fallaEmite [ScheduledMessage] Load By Ticket Failureservice.getByTicket

create$​

it()EscenarioResultado esperadoMocks/spies
dispatches createSuccess with the created itemDado create que devuelve el item, cuando llega create con ticket, body, fecha y tipoEmite createSuccess({ item })service.create
dispatches createFailure on errorDado create que fallaEmite [ScheduledMessage] Create Failureservice.create

update$​

it()EscenarioResultado esperadoMocks/spies
dispatches updateSuccess with the updated itemDado update que devuelve el item editado, cuando llega update({ id: 'sm1', body: 'edited' })update se llama con 'sm1' y { body: 'edited', scheduledAt: undefined }; emite updateSuccess({ item })service.update
dispatches updateFailure with the id on errorDado update que fallaEmite [ScheduledMessage] Update Failure con id igual a sm1service.update

cancel$​

it()EscenarioResultado esperadoMocks/spies
dispatches cancelSuccess with the idDado cancel correcto, cuando llega cancel({ id, item })cancel se llama con sm1 y emite cancelSuccess({ id: 'sm1' })service.cancel
dispatches cancelFailure carrying the original item for re-insertionDado cancel que fallaEmite cancelFailure con error (cadena), id y el item original para que el reducer lo reinserteservice.cancel

Tests en este spec: 10.

src/app/core/states/scheduled-message/scheduled-message.reducers.spec.ts​

  • SUT: scheduledMessageReducer e initialScheduledMessageState.
  • Mocking: función pura; helper makeItem; sin spies.
it()EscenarioResultado esperadoMocks/spies
sets loading true on loadMineDado el estado inicial, cuando llega loadMineloading es trueNinguno
replaces items on loadMineSuccessDado loading: true, cuando llega loadMineSuccessitems reemplazados y loading en falseNinguno
sets error on loadMineFailureDado loading: true, cuando llega loadMineFailure({ error: 'boom' })loading false y error es boomNinguno
merges loadByTicketSuccess items for that ticket without dropping items from other ticketsDado items de tk2 y un item obsoleto de tk1, cuando llega loadByTicketSuccess de tk1 con un item frescoSe conserva el item de tk2 y el de tk1 se reemplaza por el fresco (el obsoleto desaparece)Ninguno
sets saving true on createDado el estado inicial, cuando llega createsaving es trueNinguno
appends the item on createSuccess (no optimistic insert beforehand)Dado saving: true, cuando llega createSuccessitems es [item] y saving falseNinguno
does not duplicate the item on createSuccess when createdFromSocket already inserted it (race: socket beats the HTTP response)Dado que createdFromSocket ya insertó el item y luego llega createSuccess con el mismo itemitems contiene un solo item y saving es falseNinguno
replaces the item by id on updateSuccessDado un item con body old, cuando llega updateSuccess con body newitems es [updated] y saving falseNinguno
optimistically removes the item on cancelDado un item en la lista, cuando llega cancelitems queda vacíoNinguno
keeps the item removed on cancelSuccessDado un item ya removido por cancel, cuando llega cancelSuccessitems sigue vacíoNinguno
re-inserts the item on cancelFailureDado un item removido por cancel, cuando llega cancelFailure con el itemitems es [item] de nuevoNinguno
removes the item on sentFromSocket (the real bubble already landed via new_message)Dado un item pendiente, cuando llega sentFromSocketitems queda vacíoNinguno
removes the item on cancelledFromSocketDado un item pendiente, cuando llega cancelledFromSocket con razón ticket_resolveditems queda vacíoNinguno
marks the item as failed on failedFromSocket and keeps it visibleDado un item pendiente, cuando llega failedFromSocket con error boomEl item sigue en la lista con status failed y lastError boomNinguno
upserts (inserts) on createdFromSocket for an unknown idDado estado inicial, cuando llega createdFromSocketitems es [item]Ninguno
upserts (replaces) on updatedFromSocket for a known idDado un item conocido, cuando llega updatedFromSocket con body nuevoitems es [updated]Ninguno
resets to initial state on resetDado un estado sucio (items, loading, error), cuando llega resetEl estado es igual a initialScheduledMessageStateNinguno

Tests en este spec: 17.

settings​

src/app/core/states/settings/settings.effects.spec.ts​

  • SUT: SettingsEffects (loadSettings$).
  • Mocking: TestBed con provideMockActions y SettingsService espía (getSettings); helper makeSettings() con un TenantSettings completo.

loadSettings$​

it()EscenarioResultado esperadoMocks/spies
dispatches loadSettingsSuccess with the fetched settingsDado getSettings que devuelve los settings, cuando llega loadSettingsEmite loadSettingsSuccess({ settings })service.getSettings
dispatches loadSettingsFailure on errorDado getSettings que falla con boomEmite [Settings] Load Failureservice.getSettings

Tests en este spec: 2.

src/app/core/states/settings/settings.reducers.spec.ts​

  • SUT: settingsReducer e initialSettingsState.
  • Mocking: función pura; helper makeSettings(overrides).
it()EscenarioResultado esperadoMocks/spies
sets loading true on loadSettingsDado el estado inicial, cuando llega loadSettingsloading es trueNinguno
stores the settings on loadSettingsSuccessDado loading: true, cuando llega loadSettingsSuccesssettings guardados y loading falseNinguno
sets error on loadSettingsFailureDado loading: true, cuando llega loadSettingsFailure({ error: 'boom' })loading false y error es boomNinguno

Tests en este spec: 3.

ticket​

src/app/core/states/ticket/ticket.actions.spec.ts​

  • SUT: TicketActions (ticket.actions.ts).
  • Mocking: sin TestBed; se verifica type y payload de cada action creator.

Postpone​

it()EscenarioResultado esperadoMocks/spies
should create postponeTicket action carrying ticketId and postponedUntilCuando se invoca postponeTicket con ticketId y fechatype [Ticket] Postpone, con ticketId y postponedUntilNinguno
should create postponeTicketSuccess action carrying ticketId and postponedUntilCuando se invoca postponeTicketSuccesstype [Ticket] Postpone Success, con ticketId y postponedUntilNinguno
should create postponeTicketFailure action with error messageCuando se invoca postponeTicketFailure({ error: 'boom' })type [Ticket] Postpone Failure y error boomNinguno

Cancel Postpone​

it()EscenarioResultado esperadoMocks/spies
should create cancelPostponeTicket actionCuando se invoca cancelPostponeTicket({ ticketId })type [Ticket] Cancel Postpone y ticketIdNinguno
should create cancelPostponeTicketSuccess actionCuando se invoca cancelPostponeTicketSuccesstype [Ticket] Cancel Postpone Success y ticketIdNinguno
should create cancelPostponeTicketFailure action with error messageCuando se invoca cancelPostponeTicketFailuretype [Ticket] Cancel Postpone Failure y error boomNinguno

Reactivated From Socket​

it()EscenarioResultado esperadoMocks/spies
should create ticketReactivatedFromSocket actionCuando se invoca ticketReactivatedFromSocket({ ticketId })type [Ticket] Reactivated From Socket y ticketIdNinguno

Lock​

it()EscenarioResultado esperadoMocks/spies
should create lockTicket action carrying ticketIdCuando se invoca lockTickettype [Ticket] Lock y ticketIdNinguno
should create lockTicketSuccess action carrying ticketId and lockedByCuando se invoca lockTicketSuccess con lockedBy user-1type [Ticket] Lock Success, ticketId y lockedByNinguno
should create lockTicketFailure action with error messageCuando se invoca lockTicketFailuretype [Ticket] Lock Failure y errorNinguno

Unlock​

it()EscenarioResultado esperadoMocks/spies
should create unlockTicket action carrying ticketIdCuando se invoca unlockTickettype [Ticket] Unlock y ticketIdNinguno
should create unlockTicketSuccess action carrying ticketIdCuando se invoca unlockTicketSuccesstype [Ticket] Unlock Success y ticketIdNinguno
should create unlockTicketFailure action with error messageCuando se invoca unlockTicketFailuretype [Ticket] Unlock Failure y errorNinguno

Lock Changed From Socket​

it()EscenarioResultado esperadoMocks/spies
should create ticketLockChangedFromSocket action carrying ticketId, lockedBy and lockedByNameCuando se invoca con lockedBy user-1 y lockedByName Ada Lovelacetype [Ticket] Lock Changed From Socket con los tres camposNinguno
should support a null lockedBy (unlock broadcast)Cuando se invoca con lockedBy y lockedByName en nullAmbos campos son nullNinguno

Tests en este spec: 15.

src/app/core/states/ticket/ticket.effects.spec.ts​

  • SUT: TicketEffects (postponeTicket$, cancelPostponeTicket$, cancelPostponeTicketSuccess$, lockTicket$, unlockTicket$, transferTicket$, unassignTicket$, actionFailure$).
  • Mocking: TestBed con provideMockActions. Providers: TicketService espía (getTickets, postponeTicket, cancelPostponeTicket, lockTicket, unlockTicket, transferTicket, unassignTicket), ChatSocketService (sendMessage), ToastrNotificationService (showNotification), ConfirmDialogService (confirm, devuelve una promesa que nunca resuelve), TranslateService (instant devuelve la clave) y Store falso. El error de conflicto es un HttpErrorResponse 409 con data.code TICKET_LOCKED.

postponeTicket$​

it()EscenarioResultado esperadoMocks/spies
forwards ticketId and postponedUntil to the service and dispatches success with the same dateDado postponeTicket correcto, cuando llega postponeTicket con id y fechaEl servicio se llama con t1 y la fecha; emite postponeTicketSuccess con la misma fechaticketService.postponeTicket
dispatches failure when the service call failsDado postponeTicket que fallaEmite [Ticket] Postpone FailureticketService.postponeTicket

cancelPostponeTicket$​

it()EscenarioResultado esperadoMocks/spies
calls cancelPostponeTicket and dispatches successDado cancelPostponeTicket correctoSe llama con t1 y emite cancelPostponeTicketSuccess({ ticketId: 't1' })ticketService.cancelPostponeTicket
dispatches failure when cancel failsDado cancelPostponeTicket que fallaEmite [Ticket] Cancel Postpone FailureticketService.cancelPostponeTicket

cancelPostponeTicketSuccess$​

it()EscenarioResultado esperadoMocks/spies
shows a toast on cancel postpone successCuando llega cancelPostponeTicketSuccessSe llama toastr.showNotificationtoastr.showNotification

lockTicket$​

it()EscenarioResultado esperadoMocks/spies
calls lockTicket and dispatches success with the lockedBy from the responseDado lockTicket que responde data.lockedBy user-1Se llama con t1 y emite lockTicketSuccess({ ticketId: 't1', lockedBy: 'user-1' })ticketService.lockTicket
dispatches failure when the service call failsDado lockTicket que fallaEmite [Ticket] Lock FailureticketService.lockTicket

unlockTicket$​

it()EscenarioResultado esperadoMocks/spies
calls unlockTicket and dispatches successDado unlockTicket correctoSe llama con t1 y emite unlockTicketSuccess({ ticketId: 't1' })ticketService.unlockTicket
dispatches failure when the service call failsDado unlockTicket que fallaEmite [Ticket] Unlock FailureticketService.unlockTicket

transferTicket$ — locked conflict (sdd/ticket-lock W2)​

Cubre la carrera en que un bloqueo llega entre la pre-verificación del cliente y la petición: el 409 TICKET_LOCKED debe mostrar el modal de "desbloquear primero" y no el toast de error genérico.

it()EscenarioResultado esperadoMocks/spies
shows the unlock-first modal and dispatches ticketLockConflict (not transferTicketFailure) on a TICKET_LOCKED conflictDado transferTicket que falla con el 409 TICKET_LOCKEDconfirmDialog.confirm se llama con un título whaticket.ticketActions.lockedFirstTitle y se emite ticketLockConflict({ ticketId: 't1' })ticketService.transferTicket, confirmDialog.confirm
dispatches transferTicketFailure and does NOT show the unlock-first modal on any other errorDado transferTicket que falla con un error de red genéricoNo se llama confirm y se emite [Ticket] Transfer FailureticketService.transferTicket, confirmDialog.confirm

unassignTicket$ — locked conflict (sdd/ticket-lock W2)​

it()EscenarioResultado esperadoMocks/spies
shows the unlock-first modal and dispatches ticketLockConflict (not unassignTicketFailure) on a TICKET_LOCKED conflictDado unassignTicket que falla con el 409 TICKET_LOCKEDSe muestra el modal con whaticket.ticketActions.lockedFirstTitle y se emite ticketLockConflict({ ticketId: 't1' })ticketService.unassignTicket, confirmDialog.confirm
dispatches unassignTicketFailure and does NOT show the unlock-first modal on any other errorDado unassignTicket que falla con un error de red genéricoNo se llama confirm y se emite [Ticket] Unassign FailureticketService.unassignTicket, confirmDialog.confirm

ticketLockConflict is not wired into the generic actionFailure$ toast​

it()EscenarioResultado esperadoMocks/spies
actionFailure$ does not match ticketLockConflictDado que actionFailure$ está suscrito y llega ticketLockConflict, cuando pasa un setTimeout(0)El efecto no emite (fired es false) y toastr.showNotification no se llamatoastr.showNotification

Tests en este spec: 14.

src/app/core/states/ticket/ticket.reducers.spec.ts​

  • SUT: ticketReducer e initialTicketState.
  • Mocking: función pura; tickets de ejemplo (baseTicket) y estados TicketState construidos en cada caso; sin spies.

ticketReducer — Postpone / postponeTicketSuccess​

it()EscenarioResultado esperadoMocks/spies
removes the ticket from an "open"-filtered list (regression: previously reused "pending")Dado un ticket y filtro activo status: 'open', cuando llega postponeTicketSuccesstickets queda vacíoNinguno
keeps the ticket and sets status "postponed" + postponedUntil when the filter matches "postponed"Dado el filtro status: 'postponed'El ticket se conserva con status postponed y postponedUntilNinguno

ticketReducer — Postpone / cancelPostponeTicketSuccess​

it()EscenarioResultado esperadoMocks/spies
resets the ticket to "open" and clears postponedUntil when no status filter is activeDado un ticket pospuesto y filtro vacíostatus open y postponedUntil undefinedNinguno
removes the ticket when the active filter is scoped to "postponed"Dado un ticket pospuesto y filtro postponedtickets queda vacíoNinguno

ticketReducer — Postpone / ticketReactivatedFromSocket​

it()EscenarioResultado esperadoMocks/spies
sets status to "open", clears postponedUntil and flags the ticket as reactivatedDado un ticket pospuesto y filtro vacíostatus open, postponedUntil undefined y reactivated trueNinguno
removes the ticket when the active filter no longer matches "open"Dado un ticket pospuesto y filtro postponedtickets queda vacíoNinguno

ticketReducer — Postpone / ticketDeletedFromSocket​

it()EscenarioResultado esperadoMocks/spies
removes the ticket and decrements totalCount once when the ticket is presentDado un ticket presente y totalCount 1tickets vacío y totalCount 0Ninguno
is a no-op (does not decrement totalCount again) when the ticket is no longer present, e.g. the acting agent already removed it via deleteTicketSuccessDado lista vacía y totalCount 0tickets vacío y totalCount sigue en 0Ninguno
does not double-decrement when deleteTicketSuccess and ticketDeletedFromSocket both arrive for the same ticket (acting agent scenario)Dado un ticket y deleteTicketSuccess seguido del eco ticketDeletedFromSocketTras el primero totalCount es 0; tras el eco sigue en 0 y tickets vacíoNinguno
leaves totalCount unaffected relative to floor of 0 when totalCount is already 0 but ticket somehow still presentDado un ticket presente con totalCount 0El ticket se elimina y totalCount no baja de 0Ninguno

ticketReducer — Postpone / markUnreadSuccess​

it()EscenarioResultado esperadoMocks/spies
sets agentUnread=true on the matching ticketDado un ticket sin marca, cuando llega markUnreadSuccess con agentUnread: trueagentUnread es trueNinguno
is idempotent: marking an already-unread ticket again does not duplicate/increment anythingDado un ticket ya no leído, cuando llega otra vez agentUnread: trueLa lista queda igual con un solo ticketNinguno
sets agentUnread=false when clearing (opening the ticket)Dado un ticket no leído, cuando llega agentUnread: falseagentUnread es falseNinguno

ticketReducer — Postpone / contactPresenceChangedFromSocket​

La presencia se indexa por contact._id y no por ticketId, porque un contacto puede tener varias filas; el reducer debe parchear todas con .map().

it()EscenarioResultado esperadoMocks/spies
patches ALL tickets belonging to the same contact (map, not findIndex)Dado dos tickets del contacto A y uno del contacto B, cuando llega presencia online del contacto ALos dos tickets de A tienen isOnline true y lastSeen; el de B queda con ambos campos undefinedNinguno
persists lastDisconnectReason and flips isOnline false on a disconnect eventDado un contacto online, cuando llega isOnline: false con disconnectReason network-lossisOnline false, lastSeen actualizado y lastDisconnectReason network-lossNinguno
is a no-op (content-wise) when the contactId matches no ticket in stateDado un contacto desconocidotickets queda igual que antesNinguno

ticketReducer — Postpone / lockTicketSuccess​

it()EscenarioResultado esperadoMocks/spies
sets lockedBy on the matching ticketDado un ticket, cuando llega lockTicketSuccess con user-1lockedBy es user-1Ninguno
does not affect other tickets in the listDado dos tickets, cuando se bloquea t1t2 conserva lockedBy undefinedNinguno

ticketReducer — Postpone / unlockTicketSuccess​

it()EscenarioResultado esperadoMocks/spies
clears lockedBy and lockedByUser on the matching ticketDado un ticket bloqueado con lockedByUserlockedBy y lockedByUser son nullNinguno

ticketReducer — Postpone / ticketLockChangedFromSocket​

it()EscenarioResultado esperadoMocks/spies
sets lockedBy and a lockedByUser stub carrying the name when lockedDado un ticket libre, cuando llega el broadcast con user-1 y Ada LovelacelockedBy user-1 y lockedByUser.username Ada LovelaceNinguno
clears lockedBy and lockedByUser when the broadcast carries a null lockedBy (unlock)Dado un ticket bloqueado y broadcast con lockedBy nulllockedBy y lockedByUser son nullNinguno

ticketReducer — Postpone / ticketStatusChangedFromSocket — lock auto-clear (D3)​

it()EscenarioResultado esperadoMocks/spies
clears lockedBy when the new status is a terminal one (resolved/closed/cancelled)Dado un ticket inProgress bloqueado y nuevo estado resolvedlockedBy es nullNinguno
does not touch lockedBy when the new status is not terminalDado un ticket open bloqueado y nuevo estado inProgresslockedBy sigue en user-1Ninguno

ticketReducer — Operator filter ghost-row fix (sdd/chat-operator-filter D10) / unassignTicketSuccess (removeIfUnassignedMismatch)​

it()EscenarioResultado esperadoMocks/spies
removes the ticket when the active filter is scoped to operators via assignedUserIdsDado filtro assignedUserIds: ['op-1','op-2']tickets queda vacíoNinguno
APPROVAL: still removes the ticket when scoped to a single legacy assignedUserIdDado filtro legacy assignedUserId: 'op-1' (caso de aprobación del comportamiento previo)tickets queda vacíoNinguno
keeps the ticket (moved to the queue) when includeUnassigned is set alongside assignedUserIdsDado assignedUserIds con includeUnassigned: trueEl ticket permanece, status open y assignedUser undefinedNinguno
is a no-op scope-wise when no operator scope is active at allDado filtro vacíoEl ticket permanece y assignedUser es undefinedNinguno

ticketReducer — Operator filter ghost-row fix (sdd/chat-operator-filter D10) / ticketAssignedFromSocket​

it()EscenarioResultado esperadoMocks/spies
removes the ticket when it is reassigned to an operator outside the active assignedUserIds scopeDado filtro ['op-1','op-2'] y reasignación a op-9tickets queda vacíoNinguno
keeps the ticket when reassigned to an operator still inside the active assignedUserIds scopeDado el mismo filtro y reasignación a op-2El ticket permanece con assignedUser._id op-2Ninguno
APPROVAL: still removes the ticket when reassigned away from a single legacy assignedUserId scopeDado filtro legacy assignedUserId: 'op-1' y reasignación a op-9tickets queda vacíoNinguno
is a no-op scope-wise when no operator scope is active at allDado filtro vacío y reasignación a op-9El ticket permanece con assignedUser._id op-9Ninguno
drops the previous agent avatarUrl when reassigned to a different agentDado un agente con avatarUrl y reasignación a otro agenteassignedUser._id op-9 y avatarUrl undefined (evita filtrar el avatar anterior)Ninguno
keeps the avatarUrl when the socket event confirms the same agentDado el mismo agente reasignado a sí mismoavatarUrl se conservaNinguno

ticketReducer — Transfer​

it()EscenarioResultado esperadoMocks/spies
repaints the ticket with the department chosen in the transfer panelDado un ticket con departamento Jugadores VIP, cuando llega transferTicketSuccess con departamento Soporte y usuario u2department contiene d-new, Soporte y #0d6efd; assignedUser es { _id: 'u2' }Ninguno
leaves the current department untouched when the action carries noneDado transferTicketSuccess sin departmentEl departamento actual se conservaNinguno

Tests en este spec: 35.

Total​

14 specs, 140 tests (it()) documentados en esta página.

SpecTests
auth/auth.actions.spec.ts6
auth/auth.effects.spec.ts8
auth/auth.reducers.spec.ts9
auth/auth.selectors.spec.ts12
internal-chat/internal-chat.reducers.spec.ts4
language/language.effects.spec.ts3
language/language.reducers.spec.ts2
scheduled-message/scheduled-message.effects.spec.ts10
scheduled-message/scheduled-message.reducers.spec.ts17
settings/settings.effects.spec.ts2
settings/settings.reducers.spec.ts3
ticket/ticket.actions.spec.ts15
ticket/ticket.effects.spec.ts14
ticket/ticket.reducers.spec.ts35