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)) sinTestBedni espías, salvo los dos specs delanguageque espíanlocalStorage. - Efectos:
TestBed.configureTestingModuleconprovideMockActions(() => actions$)y servicios reemplazados porjasmine.createSpyObj. Cada caso asignaactions$ = of(accion)y se suscribe al efecto condone.
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 verificatypey payload.
Session & Authentication
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should create loadUserSession action | Dado el action creator, cuando se invoca loadUserSession() | type es [User] Load Session | Ninguno |
| should create userAuthenticationRequest with request payload | Dado un request con email y password, cuando se invoca userAuthenticationRequest({ request }) | type es [User] Authentication Request y action.request igual al request | Ninguno |
| should create userAuthenticationSuccess with user payload | Dado un user con _id, email y token, cuando se invoca userAuthenticationSuccess({ user }) | type es [User] Authentication Success y action.user igual al usuario | Ninguno |
| should create userAuthenticationFailure with message payload | Dado el mensaje Invalid credentials, cuando se invoca userAuthenticationFailure({ message }) | type es [User] Authentication Failure y action.message es el mensaje | Ninguno |
Logout
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should create userLogout action | Cuando se invoca userLogout() | type es [User] Logout | Ninguno |
| should create completeUserLogout action | Cuando se invoca completeUserLogout() | type es [User] Complete Logout | Ninguno |
Tests en este spec: 6.
src/app/core/states/auth/auth.effects.spec.ts
- SUT:
AuthEffects(loadUserSession$,loginUser$,logoutUser$). - Mocking:
TestBedconprovideMockActions. Providers:AuthService(login,logout),UserService(getMeUser),StorageServicecon{ secureStorage }(spy degetItem,setItem,removeItem),Router(navigate),TranslateService(instantdevuelve la clave) yStorefalso (select,dispatch).
loadUserSession$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| dispatches failure when there is no stored session | Dado que secureStorage.getItem devuelve null, cuando llega loadUserSession | Emite userAuthenticationFailure con auth.errorMessages.noActiveSession | secureStorage.getItem |
| dispatches success and merges token into user when session and API succeed | Dado una sesión guardada con token y getMeUser que responde con el usuario, cuando llega loadUserSession | Emite [User] Authentication Success con user.token igual al token guardado y user.email del usuario de la API | secureStorage.getItem, userService.getMeUser |
| dispatches failure and clears session when getMeUser returns 401 | Dado una sesión con token expirado y getMeUser que falla con Unauthorized, cuando llega loadUserSession | Emite userAuthenticationFailure con Unauthorized y se llama secureStorage.removeItem(USER_SESSION) | secureStorage.getItem, secureStorage.removeItem, userService.getMeUser |
loginUser$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| stores session, fetches user, navigates to chat and dispatches success | Dado login que responde con token y getMeUser con el usuario, cuando llega userAuthenticationRequest | Emite [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 fails | Dado login que falla con Invalid credentials, cuando llega userAuthenticationRequest | Emite userAuthenticationFailure con Invalid credentials | authService.login |
| dispatches failure when getMeUser fails after a successful login | Dado login correcto pero getMeUser que falla con Server error | Emite userAuthenticationFailure con Server error | authService.login, userService.getMeUser |
| falls back to error.message when error.error.message is absent | Dado login que falla con un error sin error.error.message (solo message: 'Network error') | Emite userAuthenticationFailure con Network error | authService.login |
logoutUser$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| calls authService.logout and dispatches completeUserLogout | Cuando llega userLogout | Se llama authService.logout y se emite [User] Complete Logout | authService.logout |
Tests en este spec: 8.
src/app/core/states/auth/auth.reducers.spec.ts
- SUT:
authReducereinitialUserState. - Mocking: función pura, sin spies.
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should return the initial state for an unknown action | Dado estado undefined y una acción desconocida @@INIT | Devuelve initialUserState | Ninguno |
loadUserSession
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should set loading to true | Dado el estado inicial, cuando llega loadUserSession | loading es true | Ninguno |
userAuthenticationRequest
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should set loading to true | Dado el estado inicial, cuando llega userAuthenticationRequest | loading es true | Ninguno |
userAuthenticationSuccess
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should store the user and set loading to false | Dado el estado inicial, cuando llega userAuthenticationSuccess con un usuario | user igual al usuario y loading es false | Ninguno |
| should overwrite a previously stored user | Dado un estado con usuario ya guardado, cuando llega un segundo success con otro email | user.email es new@test.com | Ninguno |
userAuthenticationFailure
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should set loading to false and keep user as null | Dado un estado loading: true sin usuario, cuando llega userAuthenticationFailure | loading es false y user es null | Ninguno |
| should not remove an existing user on failure | Dado un estado con usuario logueado, cuando llega userAuthenticationFailure | El usuario se conserva (el logout se maneja aparte) | Ninguno |
userLogout
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should set loading to true while logging out | Dado el estado inicial, cuando llega userLogout | loading es true | Ninguno |
completeUserLogout
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should reset state to initial values | Dado un estado con usuario logueado, cuando llega completeUserLogout | user es null y loading es false | Ninguno |
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
AppStateparcial con el helperbuildState(overrides); sin spies.
selectAuth
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should select the auth slice of state | Dado un estado con slice auth | Devuelve state.auth | Ninguno |
selectUser
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should return the current user | Dado un estado con usuario | Devuelve el usuario | Ninguno |
| should return null when no user is logged in | Dado user: null | Devuelve null | Ninguno |
selectLoading
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should return false when not loading | Dado loading: false | Devuelve false | Ninguno |
| should return true when loading | Dado loading: true | Devuelve true | Ninguno |
selectErrorMessage
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should return the current message | Dado message: 'Some error' | Devuelve Some error | Ninguno |
| should return null when message is null | Dado message: null | Devuelve null | Ninguno |
selectUserId
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should return the user _id | Dado un usuario con _id user-1 | Devuelve user-1 | Ninguno |
| should return undefined when user is null | Dado user: null | Devuelve undefined | Ninguno |
selectScheduledMessagesEnabled
El flag viene de featureFlags del estado auth/profile, no del slice settings (restringido a administradores).
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| defaults to false (fail-closed) when there is no user yet | Dado user: null | Devuelve false | Ninguno |
| defaults to false when featureFlags is missing from the user payload | Dado un usuario sin featureFlags | Devuelve false | Ninguno |
| reflects featureFlags.scheduledMessagesEnabled when present | Dado un usuario con featureFlags.scheduledMessagesEnabled en true y luego en false | Devuelve true y luego false respectivamente | Ninguno |
Tests en este spec: 12.
internal-chat
src/app/core/states/internal-chat/internal-chat.reducers.spec.ts
- SUT:
internalChatReducereinitialInternalChatState. - Mocking: función pura, sin spies; payloads de socket construidos en línea.
internalChatReducer — newMessageFromSocket / system message mapping
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| maps systemEvent/actorName/targetNames onto the appended message | Dado la conversación activa conv-1 y un mensaje de socket contentType: 'system' con systemEvent, actorName y targetNames | El mensaje añadido conserva systemEvent (member_added), actorName Ana y targetNames ['Beto'] | Ninguno |
| leaves systemEvent/actorName/targetNames undefined for a regular text message | Dado un mensaje de socket contentType: 'text' sin campos de sistema | Los tres campos quedan undefined | Ninguno |
internalChatReducer — conversationUpdatedFromSocket / unread bump guard (system flag)
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| bumps unread when the conversation is inactive and the payload is a normal message | Dado la conversación conv-1 inactiva (activa conv-2) y un payload sin system | unreadCount pasa a 1 | Ninguno |
| does NOT bump unread when the conversation is inactive but the payload carries system: true | Dado la misma conversación inactiva y un payload con system: true | unreadCount sigue en 0 y lastMessagePreview se actualiza a X added Y | Ninguno |
Tests en este spec: 4.
language
src/app/core/states/language/language.effects.spec.ts
- SUT:
LanguageEffects(loadPreviewLanguage,tenantAuthenticatedLanguage$). - Mocking:
TestBedconprovideMockActionsyTranslateServiceespía (use,setDefaultLang); el helpersetup(action)configura el módulo en cada caso.
loadPreviewLanguage
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| defaults to Spanish without reading localStorage | Cuando llega loadPreviewLanguage | Emite loadPreviewLanguageSuccess({ language: 'es' }) y no se lee la clave Language de localStorage | spyOn(localStorage, 'getItem') con callThrough |
tenantAuthenticatedLanguage$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| applies the tenant language coming from get-me | Dado userAuthenticationSuccess con tenantPreferences.language en | Emite change({ language: 'en' }) | TranslateService espía |
| falls back to Spanish when the tenant language is missing or unsupported | Dado 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() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| stores the language without persisting it to localStorage | Dado { code: 'es' }, cuando llega change({ language: 'en' }) | code es en y localStorage.setItem no se llama | spyOn(localStorage, 'setItem') |
| stores the language coming from the bootstrap effect | Dado { code: 'es' }, cuando llega loadPreviewLanguageSuccess({ language: 'en' }) | code es en | Ninguno |
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:
TestBedconprovideMockActionsyScheduledMessageServiceespía (create,getMine,getByTicket,update,cancel); helpermakeItempara el mensaje programado.
loadMine$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| dispatches loadMineSuccess with the results | Dado 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 error | Dado getMine que falla con boom | Emite una acción de tipo [ScheduledMessage] Load Mine Failure | service.getMine |
loadByTicket$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| dispatches loadByTicketSuccess with the ticketId and results | Dado getByTicket que devuelve [item], cuando llega loadByTicket({ ticketId: 'tk1' }) | getByTicket se llama con tk1 y se emite loadByTicketSuccess con ticketId e items | service.getByTicket |
| dispatches loadByTicketFailure on error | Dado getByTicket que falla | Emite [ScheduledMessage] Load By Ticket Failure | service.getByTicket |
create$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| dispatches createSuccess with the created item | Dado create que devuelve el item, cuando llega create con ticket, body, fecha y tipo | Emite createSuccess({ item }) | service.create |
| dispatches createFailure on error | Dado create que falla | Emite [ScheduledMessage] Create Failure | service.create |
update$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| dispatches updateSuccess with the updated item | Dado 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 error | Dado update que falla | Emite [ScheduledMessage] Update Failure con id igual a sm1 | service.update |
cancel$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| dispatches cancelSuccess with the id | Dado 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-insertion | Dado cancel que falla | Emite cancelFailure con error (cadena), id y el item original para que el reducer lo reinserte | service.cancel |
Tests en este spec: 10.
src/app/core/states/scheduled-message/scheduled-message.reducers.spec.ts
- SUT:
scheduledMessageReducereinitialScheduledMessageState. - Mocking: función pura; helper
makeItem; sin spies.
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| sets loading true on loadMine | Dado el estado inicial, cuando llega loadMine | loading es true | Ninguno |
| replaces items on loadMineSuccess | Dado loading: true, cuando llega loadMineSuccess | items reemplazados y loading en false | Ninguno |
| sets error on loadMineFailure | Dado loading: true, cuando llega loadMineFailure({ error: 'boom' }) | loading false y error es boom | Ninguno |
| merges loadByTicketSuccess items for that ticket without dropping items from other tickets | Dado items de tk2 y un item obsoleto de tk1, cuando llega loadByTicketSuccess de tk1 con un item fresco | Se conserva el item de tk2 y el de tk1 se reemplaza por el fresco (el obsoleto desaparece) | Ninguno |
| sets saving true on create | Dado el estado inicial, cuando llega create | saving es true | Ninguno |
| appends the item on createSuccess (no optimistic insert beforehand) | Dado saving: true, cuando llega createSuccess | items es [item] y saving false | Ninguno |
| 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 item | items contiene un solo item y saving es false | Ninguno |
| replaces the item by id on updateSuccess | Dado un item con body old, cuando llega updateSuccess con body new | items es [updated] y saving false | Ninguno |
| optimistically removes the item on cancel | Dado un item en la lista, cuando llega cancel | items queda vacío | Ninguno |
| keeps the item removed on cancelSuccess | Dado un item ya removido por cancel, cuando llega cancelSuccess | items sigue vacío | Ninguno |
| re-inserts the item on cancelFailure | Dado un item removido por cancel, cuando llega cancelFailure con el item | items es [item] de nuevo | Ninguno |
| removes the item on sentFromSocket (the real bubble already landed via new_message) | Dado un item pendiente, cuando llega sentFromSocket | items queda vacío | Ninguno |
| removes the item on cancelledFromSocket | Dado un item pendiente, cuando llega cancelledFromSocket con razón ticket_resolved | items queda vacío | Ninguno |
| marks the item as failed on failedFromSocket and keeps it visible | Dado un item pendiente, cuando llega failedFromSocket con error boom | El item sigue en la lista con status failed y lastError boom | Ninguno |
| upserts (inserts) on createdFromSocket for an unknown id | Dado estado inicial, cuando llega createdFromSocket | items es [item] | Ninguno |
| upserts (replaces) on updatedFromSocket for a known id | Dado un item conocido, cuando llega updatedFromSocket con body nuevo | items es [updated] | Ninguno |
| resets to initial state on reset | Dado un estado sucio (items, loading, error), cuando llega reset | El estado es igual a initialScheduledMessageState | Ninguno |
Tests en este spec: 17.
settings
src/app/core/states/settings/settings.effects.spec.ts
- SUT:
SettingsEffects(loadSettings$). - Mocking:
TestBedconprovideMockActionsySettingsServiceespía (getSettings); helpermakeSettings()con unTenantSettingscompleto.
loadSettings$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| dispatches loadSettingsSuccess with the fetched settings | Dado getSettings que devuelve los settings, cuando llega loadSettings | Emite loadSettingsSuccess({ settings }) | service.getSettings |
| dispatches loadSettingsFailure on error | Dado getSettings que falla con boom | Emite [Settings] Load Failure | service.getSettings |
Tests en este spec: 2.
src/app/core/states/settings/settings.reducers.spec.ts
- SUT:
settingsReducereinitialSettingsState. - Mocking: función pura; helper
makeSettings(overrides).
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| sets loading true on loadSettings | Dado el estado inicial, cuando llega loadSettings | loading es true | Ninguno |
| stores the settings on loadSettingsSuccess | Dado loading: true, cuando llega loadSettingsSuccess | settings guardados y loading false | Ninguno |
| sets error on loadSettingsFailure | Dado loading: true, cuando llega loadSettingsFailure({ error: 'boom' }) | loading false y error es boom | Ninguno |
Tests en este spec: 3.
ticket
src/app/core/states/ticket/ticket.actions.spec.ts
- SUT:
TicketActions(ticket.actions.ts). - Mocking: sin
TestBed; se verificatypey payload de cada action creator.
Postpone
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should create postponeTicket action carrying ticketId and postponedUntil | Cuando se invoca postponeTicket con ticketId y fecha | type [Ticket] Postpone, con ticketId y postponedUntil | Ninguno |
| should create postponeTicketSuccess action carrying ticketId and postponedUntil | Cuando se invoca postponeTicketSuccess | type [Ticket] Postpone Success, con ticketId y postponedUntil | Ninguno |
| should create postponeTicketFailure action with error message | Cuando se invoca postponeTicketFailure({ error: 'boom' }) | type [Ticket] Postpone Failure y error boom | Ninguno |
Cancel Postpone
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should create cancelPostponeTicket action | Cuando se invoca cancelPostponeTicket({ ticketId }) | type [Ticket] Cancel Postpone y ticketId | Ninguno |
| should create cancelPostponeTicketSuccess action | Cuando se invoca cancelPostponeTicketSuccess | type [Ticket] Cancel Postpone Success y ticketId | Ninguno |
| should create cancelPostponeTicketFailure action with error message | Cuando se invoca cancelPostponeTicketFailure | type [Ticket] Cancel Postpone Failure y error boom | Ninguno |
Reactivated From Socket
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should create ticketReactivatedFromSocket action | Cuando se invoca ticketReactivatedFromSocket({ ticketId }) | type [Ticket] Reactivated From Socket y ticketId | Ninguno |
Lock
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should create lockTicket action carrying ticketId | Cuando se invoca lockTicket | type [Ticket] Lock y ticketId | Ninguno |
| should create lockTicketSuccess action carrying ticketId and lockedBy | Cuando se invoca lockTicketSuccess con lockedBy user-1 | type [Ticket] Lock Success, ticketId y lockedBy | Ninguno |
| should create lockTicketFailure action with error message | Cuando se invoca lockTicketFailure | type [Ticket] Lock Failure y error | Ninguno |
Unlock
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should create unlockTicket action carrying ticketId | Cuando se invoca unlockTicket | type [Ticket] Unlock y ticketId | Ninguno |
| should create unlockTicketSuccess action carrying ticketId | Cuando se invoca unlockTicketSuccess | type [Ticket] Unlock Success y ticketId | Ninguno |
| should create unlockTicketFailure action with error message | Cuando se invoca unlockTicketFailure | type [Ticket] Unlock Failure y error | Ninguno |
Lock Changed From Socket
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| should create ticketLockChangedFromSocket action carrying ticketId, lockedBy and lockedByName | Cuando se invoca con lockedBy user-1 y lockedByName Ada Lovelace | type [Ticket] Lock Changed From Socket con los tres campos | Ninguno |
| should support a null lockedBy (unlock broadcast) | Cuando se invoca con lockedBy y lockedByName en null | Ambos campos son null | Ninguno |
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:
TestBedconprovideMockActions. Providers:TicketServiceespía (getTickets,postponeTicket,cancelPostponeTicket,lockTicket,unlockTicket,transferTicket,unassignTicket),ChatSocketService(sendMessage),ToastrNotificationService(showNotification),ConfirmDialogService(confirm, devuelve una promesa que nunca resuelve),TranslateService(instantdevuelve la clave) yStorefalso. El error de conflicto es unHttpErrorResponse409 condata.codeTICKET_LOCKED.
postponeTicket$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| forwards ticketId and postponedUntil to the service and dispatches success with the same date | Dado postponeTicket correcto, cuando llega postponeTicket con id y fecha | El servicio se llama con t1 y la fecha; emite postponeTicketSuccess con la misma fecha | ticketService.postponeTicket |
| dispatches failure when the service call fails | Dado postponeTicket que falla | Emite [Ticket] Postpone Failure | ticketService.postponeTicket |
cancelPostponeTicket$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| calls cancelPostponeTicket and dispatches success | Dado cancelPostponeTicket correcto | Se llama con t1 y emite cancelPostponeTicketSuccess({ ticketId: 't1' }) | ticketService.cancelPostponeTicket |
| dispatches failure when cancel fails | Dado cancelPostponeTicket que falla | Emite [Ticket] Cancel Postpone Failure | ticketService.cancelPostponeTicket |
cancelPostponeTicketSuccess$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| shows a toast on cancel postpone success | Cuando llega cancelPostponeTicketSuccess | Se llama toastr.showNotification | toastr.showNotification |
lockTicket$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| calls lockTicket and dispatches success with the lockedBy from the response | Dado lockTicket que responde data.lockedBy user-1 | Se llama con t1 y emite lockTicketSuccess({ ticketId: 't1', lockedBy: 'user-1' }) | ticketService.lockTicket |
| dispatches failure when the service call fails | Dado lockTicket que falla | Emite [Ticket] Lock Failure | ticketService.lockTicket |
unlockTicket$
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| calls unlockTicket and dispatches success | Dado unlockTicket correcto | Se llama con t1 y emite unlockTicketSuccess({ ticketId: 't1' }) | ticketService.unlockTicket |
| dispatches failure when the service call fails | Dado unlockTicket que falla | Emite [Ticket] Unlock Failure | ticketService.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() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| shows the unlock-first modal and dispatches ticketLockConflict (not transferTicketFailure) on a TICKET_LOCKED conflict | Dado transferTicket que falla con el 409 TICKET_LOCKED | confirmDialog.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 error | Dado transferTicket que falla con un error de red genérico | No se llama confirm y se emite [Ticket] Transfer Failure | ticketService.transferTicket, confirmDialog.confirm |
unassignTicket$ — locked conflict (sdd/ticket-lock W2)
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| shows the unlock-first modal and dispatches ticketLockConflict (not unassignTicketFailure) on a TICKET_LOCKED conflict | Dado unassignTicket que falla con el 409 TICKET_LOCKED | Se 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 error | Dado unassignTicket que falla con un error de red genérico | No se llama confirm y se emite [Ticket] Unassign Failure | ticketService.unassignTicket, confirmDialog.confirm |
ticketLockConflict is not wired into the generic actionFailure$ toast
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| actionFailure$ does not match ticketLockConflict | Dado que actionFailure$ está suscrito y llega ticketLockConflict, cuando pasa un setTimeout(0) | El efecto no emite (fired es false) y toastr.showNotification no se llama | toastr.showNotification |
Tests en este spec: 14.
src/app/core/states/ticket/ticket.reducers.spec.ts
- SUT:
ticketReducereinitialTicketState. - Mocking: función pura; tickets de ejemplo (
baseTicket) y estadosTicketStateconstruidos en cada caso; sin spies.
ticketReducer — Postpone / postponeTicketSuccess
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| removes the ticket from an "open"-filtered list (regression: previously reused "pending") | Dado un ticket y filtro activo status: 'open', cuando llega postponeTicketSuccess | tickets queda vacío | Ninguno |
| 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 postponedUntil | Ninguno |
ticketReducer — Postpone / cancelPostponeTicketSuccess
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| resets the ticket to "open" and clears postponedUntil when no status filter is active | Dado un ticket pospuesto y filtro vacío | status open y postponedUntil undefined | Ninguno |
| removes the ticket when the active filter is scoped to "postponed" | Dado un ticket pospuesto y filtro postponed | tickets queda vacío | Ninguno |
ticketReducer — Postpone / ticketReactivatedFromSocket
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| sets status to "open", clears postponedUntil and flags the ticket as reactivated | Dado un ticket pospuesto y filtro vacío | status open, postponedUntil undefined y reactivated true | Ninguno |
| removes the ticket when the active filter no longer matches "open" | Dado un ticket pospuesto y filtro postponed | tickets queda vacío | Ninguno |
ticketReducer — Postpone / ticketDeletedFromSocket
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| removes the ticket and decrements totalCount once when the ticket is present | Dado un ticket presente y totalCount 1 | tickets vacío y totalCount 0 | Ninguno |
| 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 deleteTicketSuccess | Dado lista vacía y totalCount 0 | tickets vacío y totalCount sigue en 0 | Ninguno |
| 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 ticketDeletedFromSocket | Tras el primero totalCount es 0; tras el eco sigue en 0 y tickets vacío | Ninguno |
| leaves totalCount unaffected relative to floor of 0 when totalCount is already 0 but ticket somehow still present | Dado un ticket presente con totalCount 0 | El ticket se elimina y totalCount no baja de 0 | Ninguno |
ticketReducer — Postpone / markUnreadSuccess
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| sets agentUnread=true on the matching ticket | Dado un ticket sin marca, cuando llega markUnreadSuccess con agentUnread: true | agentUnread es true | Ninguno |
| is idempotent: marking an already-unread ticket again does not duplicate/increment anything | Dado un ticket ya no leído, cuando llega otra vez agentUnread: true | La lista queda igual con un solo ticket | Ninguno |
| sets agentUnread=false when clearing (opening the ticket) | Dado un ticket no leído, cuando llega agentUnread: false | agentUnread es false | Ninguno |
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() | Escenario | Resultado esperado | Mocks/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 A | Los dos tickets de A tienen isOnline true y lastSeen; el de B queda con ambos campos undefined | Ninguno |
| persists lastDisconnectReason and flips isOnline false on a disconnect event | Dado un contacto online, cuando llega isOnline: false con disconnectReason network-loss | isOnline false, lastSeen actualizado y lastDisconnectReason network-loss | Ninguno |
| is a no-op (content-wise) when the contactId matches no ticket in state | Dado un contacto desconocido | tickets queda igual que antes | Ninguno |
ticketReducer — Postpone / lockTicketSuccess
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| sets lockedBy on the matching ticket | Dado un ticket, cuando llega lockTicketSuccess con user-1 | lockedBy es user-1 | Ninguno |
| does not affect other tickets in the list | Dado dos tickets, cuando se bloquea t1 | t2 conserva lockedBy undefined | Ninguno |
ticketReducer — Postpone / unlockTicketSuccess
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| clears lockedBy and lockedByUser on the matching ticket | Dado un ticket bloqueado con lockedByUser | lockedBy y lockedByUser son null | Ninguno |
ticketReducer — Postpone / ticketLockChangedFromSocket
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| sets lockedBy and a lockedByUser stub carrying the name when locked | Dado un ticket libre, cuando llega el broadcast con user-1 y Ada Lovelace | lockedBy user-1 y lockedByUser.username Ada Lovelace | Ninguno |
| clears lockedBy and lockedByUser when the broadcast carries a null lockedBy (unlock) | Dado un ticket bloqueado y broadcast con lockedBy null | lockedBy y lockedByUser son null | Ninguno |
ticketReducer — Postpone / ticketStatusChangedFromSocket — lock auto-clear (D3)
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| clears lockedBy when the new status is a terminal one (resolved/closed/cancelled) | Dado un ticket inProgress bloqueado y nuevo estado resolved | lockedBy es null | Ninguno |
| does not touch lockedBy when the new status is not terminal | Dado un ticket open bloqueado y nuevo estado inProgress | lockedBy sigue en user-1 | Ninguno |
ticketReducer — Operator filter ghost-row fix (sdd/chat-operator-filter D10) / unassignTicketSuccess (removeIfUnassignedMismatch)
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| removes the ticket when the active filter is scoped to operators via assignedUserIds | Dado filtro assignedUserIds: ['op-1','op-2'] | tickets queda vacío | Ninguno |
| APPROVAL: still removes the ticket when scoped to a single legacy assignedUserId | Dado filtro legacy assignedUserId: 'op-1' (caso de aprobación del comportamiento previo) | tickets queda vacío | Ninguno |
| keeps the ticket (moved to the queue) when includeUnassigned is set alongside assignedUserIds | Dado assignedUserIds con includeUnassigned: true | El ticket permanece, status open y assignedUser undefined | Ninguno |
| is a no-op scope-wise when no operator scope is active at all | Dado filtro vacío | El ticket permanece y assignedUser es undefined | Ninguno |
ticketReducer — Operator filter ghost-row fix (sdd/chat-operator-filter D10) / ticketAssignedFromSocket
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| removes the ticket when it is reassigned to an operator outside the active assignedUserIds scope | Dado filtro ['op-1','op-2'] y reasignación a op-9 | tickets queda vacío | Ninguno |
| keeps the ticket when reassigned to an operator still inside the active assignedUserIds scope | Dado el mismo filtro y reasignación a op-2 | El ticket permanece con assignedUser._id op-2 | Ninguno |
| APPROVAL: still removes the ticket when reassigned away from a single legacy assignedUserId scope | Dado filtro legacy assignedUserId: 'op-1' y reasignación a op-9 | tickets queda vacío | Ninguno |
| is a no-op scope-wise when no operator scope is active at all | Dado filtro vacío y reasignación a op-9 | El ticket permanece con assignedUser._id op-9 | Ninguno |
| drops the previous agent avatarUrl when reassigned to a different agent | Dado un agente con avatarUrl y reasignación a otro agente | assignedUser._id op-9 y avatarUrl undefined (evita filtrar el avatar anterior) | Ninguno |
| keeps the avatarUrl when the socket event confirms the same agent | Dado el mismo agente reasignado a sí mismo | avatarUrl se conserva | Ninguno |
ticketReducer — Transfer
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
| repaints the ticket with the department chosen in the transfer panel | Dado un ticket con departamento Jugadores VIP, cuando llega transferTicketSuccess con departamento Soporte y usuario u2 | department contiene d-new, Soporte y #0d6efd; assignedUser es { _id: 'u2' } | Ninguno |
| leaves the current department untouched when the action carries none | Dado transferTicketSuccess sin department | El departamento actual se conserva | Ninguno |
Tests en este spec: 35.
Total
14 specs, 140 tests (it()) documentados en esta página.
| Spec | Tests |
|---|---|
auth/auth.actions.spec.ts | 6 |
auth/auth.effects.spec.ts | 8 |
auth/auth.reducers.spec.ts | 9 |
auth/auth.selectors.spec.ts | 12 |
internal-chat/internal-chat.reducers.spec.ts | 4 |
language/language.effects.spec.ts | 3 |
language/language.reducers.spec.ts | 2 |
scheduled-message/scheduled-message.effects.spec.ts | 10 |
scheduled-message/scheduled-message.reducers.spec.ts | 17 |
settings/settings.effects.spec.ts | 2 |
settings/settings.reducers.spec.ts | 3 |
ticket/ticket.actions.spec.ts | 15 |
ticket/ticket.effects.spec.ts | 14 |
ticket/ticket.reducers.spec.ts | 35 |