Features de cuenta y configuración
Casos de prueba unitarios (Karma + Jasmine) de las features auth, profile y settings bajo src/app/features. Son 11 specs y 90 it(). No hay xit, fdescribe, fit ni xdescribe, y ningún spec está vacío.
Los títulos de los it() se citan literalmente (en inglés, como en el código); el escenario y el resultado se describen en español.
auth
src/app/features/auth/pages/login/login.component.spec.ts
- SUT:
LoginComponent(pantalla de login con formulario reactivo). - Mocking:
TestBedcondeclarations: [LoginComponent],ReactiveFormsModule,TranslateModule.forRootconTranslateFakeLoader,provideMockStore({ initialState })(estadoauthconuser: null,loading: false,message: null) yprovideMockActionsalimentado por unSubject<Action>(actionsSubject) que se completa enafterEach. Se espíastore.dispatchconspyOn.
describe LoginComponent
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
should create | Dado el componente creado con detectChanges, cuando se inspecciona la instancia. | La instancia existe. | MockStore, provideMockActions |
should build a form with email and password controls | Dado el componente inicializado, cuando se consultan los controles. | Existen los controles email y password. | — |
should start with an invalid form | Dado el formulario recién construido (campos vacíos). | El formulario es inválido. | — |
should start with showPassword = false | Dado el componente recién creado. | showPassword es false. | — |
should be invalid with a non-email value | Cuando se asigna email: 'not-an-email' con contraseña válida (caso borde de formato). | El control email es inválido. | — |
should be invalid when password is empty | Cuando se asigna un email válido y password: ''. | El control password es inválido. | — |
should be valid with a correct email and any non-empty password | Cuando se asigna user@example.com y secret. | El formulario completo es válido. | — |
should NOT dispatch when the form is invalid | Dado un formulario con campos vacíos, cuando se llama onSubmit(). | No se llama a dispatch. | spyOn(store, 'dispatch') |
should dispatch userAuthenticationRequest with form values on valid submit | Dado un formulario válido, cuando se llama onSubmit(). | Se despacha UserActions.userAuthenticationRequest con request: { email, password }. | spyOn(store, 'dispatch') |
should clear the error observable when form is submitted | Dado un error previo emitido con userAuthenticationFailure ('Previous error'), cuando se reenvía el formulario válido. | error$ emite null. | actionsSubject.next(...) |
should toggle showPassword on each togglePassword call | Dado showPassword en false, cuando se invoca togglePassword() dos veces. | Pasa a true y vuelve a false. | — |
should emit the error message when userAuthenticationFailure fires | Cuando la acción userAuthenticationFailure con 'Invalid credentials' llega por el stream de actions (ruta de fallo). | error$ emite 'Invalid credentials'. | actionsSubject.next(...) |
should start with a null error$ | Dado el componente recién creado. | error$ emite null. | — |
should reflect store loading state in loading$ | Cuando el store cambia a auth.loading: true. | loading$ emite true. | store.setState(...) |
should complete cleanly on destroy without errors | Cuando se llama ngOnDestroy(). | No lanza excepción. | — |
Total del spec: 15 tests.
profile
src/app/features/profile/components/avatar-cropper-dialog/avatar-cropper-dialog.component.spec.ts
- SUT:
AvatarCropperDialogComponent(diálogo de recorte de avatar). - Mocking:
TestBedconFormsModule,TranslateModule.forRoot()eImageCropperComponentimportado;NgbActiveModalreemplazado porjasmine.createSpyObj('NgbActiveModal', ['close', 'dismiss']). Se crea el componente sinfixture.detectChanges()para no renderizar el cropper real; el evento de recorte se construye a mano (ImageCroppedEvent) y se pasa aonImageCropped().imageFilees unFileJPG sintético.
describe onZoomChange (D4 — zoom slider clamped to [1,3], new transform object)
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
clamps a value above MAX_SCALE down to 3 and reassigns transform with a NEW object | Cuando se llama onZoomChange(5) (por encima del máximo). | transform.scale es 3 y transform es un objeto distinto al anterior. | — |
clamps a value below MIN_SCALE up to 1 (never smaller than original size) | Cuando se llama onZoomChange(0) (por debajo del mínimo). | transform.scale es 1 y transform es un objeto nuevo. | — |
describe onTransformChange (D5 — dragging must not be reset by the next zoom change)
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
keeps the drag translate values from the (transformChange) output | Cuando se llama onTransformChange({ scale: 2, translateH: 15, translateV: -7 }). | translateH es 15 y translateV es -7. | — |
preserves the translate offset after a subsequent zoom change (main D5 gotcha) | Dado un arrastre previo (translateH: 40, translateV: 22), cuando se hace onZoomChange(2). | La escala es 2 y las traslaciones se conservan (40, 22). | — |
describe onConfirm
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
does NOT close the modal when no cropped blob is available yet | Dado que aún no hubo evento de recorte, cuando se llama onConfirm(). | activeModal.close no se invoca. | activeModal spy |
does NOT close the modal when the latest ImageCroppedEvent has a null blob | Dado un ImageCroppedEvent con blob: null, cuando se confirma. | activeModal.close no se invoca. | activeModal spy |
closes with a File (image/jpeg) built from the latest cropped blob | Dado un evento con un Blob image/jpeg, cuando se confirma. | close se llama una vez con un File de tipo image/jpeg. | activeModal spy |
describe onCancel
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
dismisses the modal with null | Cuando se llama onCancel(). | activeModal.dismiss se llama con null. | activeModal spy |
Total del spec: 8 tests.
src/app/features/profile/components/avatar-cropper-dialog/avatar-cropper-dialog.service.spec.ts
- SUT:
AvatarCropperDialogService. - Mocking: instanciación directa
new AvatarCropperDialogService(modal)conjasmine.createSpyObj('NgbModal', ['open'])que devuelve unaNgbModalReffalsa (componentInstanceyresult). No se renderiza ningún modal.
describe AvatarCropperDialogService
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
passes the file to componentInstance.imageFile | Dado un result que nunca se resuelve, cuando se llama service.open(file). | componentInstance.imageFile es el mismo File. | modal.open devuelve ref falsa |
resolves with the closed File when ref.result resolves | Dado result resuelto con un File recortado. | open() resuelve con ese File. | modal.open devuelve ref falsa |
resolves with null when ref.result rejects (dismiss/cancel) | Dado result rechazado con null (descarte/cancelación, ruta de fallo). | open() resuelve con null. | modal.open devuelve ref falsa |
Total del spec: 3 tests.
src/app/features/profile/pages/profile-page/profile-page.component.spec.ts
- SUT:
ProfilePageComponent(carga de avatar con diálogo de recorte, métodoonAvatarSelected). - Mocking: instanciación directa
new ProfilePageComponent(new FormBuilder(), store, userService, notification, new UserAvatarsService(), avatarCropperDialogService), conjasmine.createSpyObjparaStore(selectdevuelveof(currentUser),dispatch),UserService(updateMySignature,updateMyAvatar),ToastrNotificationServiceyAvatarCropperDialogService(open). Se llamangOnInit()enbeforeEachyngOnDestroy()enafterEach. El archivo se simula con uninput[type=file]yDataTransfer.
describe ProfilePageComponent — avatar upload (with crop dialog)
describe reject non-JPG file (client-side mirror of backend validation)
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
never opens the crop dialog nor calls updateMyAvatar for a PNG | Dado un archivo image/png, cuando se selecciona. | No se abre el diálogo, no se sube, no se hace dispatch y avatarError queda con valor. | avatarCropperDialogService, userService, store |
describe reject oversized file (client-side mirror of backend validation)
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
never opens the crop dialog nor calls updateMyAvatar for a file over the 2MB cap | Dado un JPG de 2 MB + 1 byte (caso borde del límite). | No se abre el diálogo ni se sube; avatarError queda con valor. | avatarCropperDialogService, userService |
describe dialog cancelled
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
does not upload, leaves avatarPreviewUrl unchanged, and does not dispatch when the dialog resolves null | Dado un JPG válido y el diálogo que resuelve null (cancelado). | Se abrió el diálogo, no se sube ni se despacha y avatarPreviewUrl no cambia. | open devuelve Promise.resolve(null) |
describe dialog confirmed with a cropped file
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
uploads the CROPPED file (not the original) and dispatches a MERGED user (permissions preserved — D8) | Dado que el diálogo devuelve el archivo recortado y updateMyAvatar responde con un avatarUrl nuevo. | Se sube el archivo recortado; se despacha UserActions.userProfileUpdateSuccess con el usuario fusionado con currentUser y permissions intactos. | open, updateMyAvatar (of(...)), store.dispatch |
sets the local preview from the CROPPED file only after confirm (D7) | Dado avatarPreviewUrl en null y un recorte confirmado. | Tras seleccionar, avatarPreviewUrl coincide con /^blob:/. | open, updateMyAvatar |
clears the busy flag once the upload resolves | Dado un recorte confirmado y subida exitosa. | isUploadingAvatar es false. | open, updateMyAvatar |
describe upload failure after confirm
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
revokes the preview and surfaces an error without dispatching | Dado un recorte confirmado y updateMyAvatar que falla con Error('network'). | No se despacha; avatarPreviewUrl es null, avatarError tiene valor e isUploadingAvatar es false. | updateMyAvatar con throwError |
describe no file selected
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
is a no-op | Dado un input sin archivos (null). | No se abre el diálogo ni se llama updateMyAvatar. | avatarCropperDialogService, userService |
Total del spec: 8 tests.
src/app/features/profile/starred-messages/pages/starred-messages-page/starred-messages-page.component.spec.ts
- SUT:
StarredMessagesPageComponent(listado global de mensajes destacados). - Mocking: instanciación directa
new StarredMessagesPageComponent(starredMessageService, router, toastr).StarredMessageServicees unjasmine.createSpyObjcon métodofindGlobaly propiedadchanged$(unSubjectexpuesto como observable);Routerconnavigate;ToastrNotificationServiceconshowNotification. El helperlistItem(overrides)construye unStarredMessageListItem.
describe load
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
loads the first page (mixed contact + internal) | Dado que findGlobal devuelve dos resultados (uno contact, uno internal), cuando se llama load(true). | findGlobal se llama con (1, 20, undefined) y items tiene 2 elementos. | findGlobal con of(...) |
resets to empty on error | Dado que findGlobal falla con Error('boom'). | items queda vacío e isLoading es false. | findGlobal con throwError |
describe refresh after a toggle elsewhere
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
reloads the list from page 1 when a message is starred or un-starred | Dado ngOnInit() ejecutado y la cuenta de llamadas reiniciada, cuando changed$ emite un cambio. | findGlobal se llama una vez, con página 1. | changed$ (Subject), findGlobal.calls.reset() |
stops listening once the page is destroyed | Dado ngOnInit() y luego ngOnDestroy(), cuando changed$ emite. | findGlobal no se llama. | changed$, findGlobal |
describe setFilter
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
reloads page 1 scoped to the chosen messageType | Cuando se llama setFilter('internal'). | activeFilter es 'internal' y findGlobal se llama con (1, 20, 'internal'). | findGlobal con of(...) |
is a no-op when re-selecting the already-active filter | Dado activeFilter = 'contact', cuando se vuelve a seleccionar 'contact'. | findGlobal no se llama. | findGlobal |
describe getPreview
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
renders the hydrated file filename when a file is present (WU5) | Dado un item con file.filename = 'foto.png'. | Devuelve 'foto.png'. | — |
falls back to "[archivo]" when the file has no filename | Dado un item con archivo sin filename. | Devuelve '[archivo]'. | — |
renders the raw body when there is no attached file | Dado un item sin archivo y message.body = 'texto'. | Devuelve 'texto'. | — |
describe getSenderName
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
returns firstname + lastName when both are present | Dado un remitente con firstname: 'Alan' y lastName: 'Turing'. | Devuelve 'Alan Turing'. | — |
falls back to username when firstname/lastName are missing | Dado un remitente solo con username: 'alan'. | Devuelve 'alan'. | — |
returns an empty string when sender is undefined | Dado sender: undefined. | Devuelve ''. | — |
describe hasFile / isFileImage / getFileIconClass
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
reports no file when file is null | Dado file: null. | hasFile devuelve false. | — |
reports a file and detects a video mimetype (non-image icon) | Dado un archivo con mimetype: 'video/mp4'. | hasFile es true, isFileImage es false y getFileIconClass devuelve 'fa-video-camera'. | — |
detects an image mimetype | Dado un archivo con mimetype: 'image/webp'. | isFileImage es true. | — |
describe openItem — click branching (task 4.5)
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
navigates to whaticket chat with the ticketId + highlightMessageId for a contact item | Dado un item contact con ticketId: 'ticket-99' y messageId: 'msg-42'. | router.navigate(['/whaticket/chat'], { queryParams: { chatId: 'ticket-99', highlightMessageId: 'msg-42' } }). | router.navigate |
navigates to internal-chat with the conversationId + highlightMessageId for an internal item | Dado un item internal con conversationId: 'conv-5' y messageId: 'msg-77'. | router.navigate(['/internal-chat/conversations'], { queryParams: { conversationId: 'conv-5', highlightMessageId: 'msg-77' } }). | router.navigate |
shows an error toast instead of navigating when a contact item has no ticketId (orphan) | Dado un item contact huérfano sin ticketId. | No navega y se llama toastr.showNotification. | router.navigate, toastr.showNotification |
shows an error toast instead of navigating when an internal item has no conversationId | Dado un item internal con conversationId: undefined. | No navega y se llama toastr.showNotification. | router.navigate, toastr.showNotification |
Total del spec: 19 tests.
Total de la sección profile: 38 tests (4 specs).
settings
src/app/features/settings/components/auto-resolution-panel/auto-resolution-panel.component.spec.ts
- SUT:
AutoResolutionPanelComponent(panel de autorresolución de tickets). - Mocking:
TestBedconReactiveFormsModule,TranslateModule.forRoot(),SwitchModule,RichTextEditorModuleyNO_ERRORS_SCHEMA;SettingsServicecomojasmine.createSpyObjconupdateAutoResolutionyToastrNotificationServiceconshowNotification. HelpersmakeAutoResolution(overrides)ymakeTenantSettings(autoResolution). La entradasettingsse asigna antes defixture.detectChanges().
describe AutoResolutionPanelComponent
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
should populate the form from the settings input | Dado settings con resolveInMinutes: 15 y enabled: true. | El formulario refleja 15 y true. | — |
should not expose a farewellMessage control | Dado un settings válido. | form.get('farewellMessage') es null. | — |
should call SettingsService.updateAutoResolution with the form payload on save | Dado notifyInMinutes cambiado a 3, cuando se llama onSave(). | updateAutoResolution se invoca con un payload cuyo notifyInMinutes es 3. | updateAutoResolution con of(...) |
should send sendFarewellMessage in the save payload without any farewell text | Dado sendFarewellMessage: false, cuando se guarda. | El payload lleva sendFarewellMessage: false y no contiene farewellMessage. | updateAutoResolution con of(...) |
should emit saved with the updated autoResolution section | Dado que el servicio devuelve notificationMessage: 'Nuevo mensaje', cuando se guarda. | El output saved emite la sección actualizada. | savedSpy, updateAutoResolution |
should reset the form to the last saved snapshot on cancel | Dado resolveInMinutes modificado a 99 sin guardar, cuando se llama onCancel(). | El control vuelve a 10. | — |
should set isSaving back to false when the save request fails | Dado que updateAutoResolution falla con Error('boom') (ruta de fallo). | isSaving es false. | updateAutoResolution con throwError |
describe validations
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
requires resolveInMinutes | Dado resolveInMinutes en null. | El control tiene el error required y el formulario es inválido. | — |
rejects times below one minute | Dado resolveInMinutes: 0 y notifyInMinutes: 0. | Ambos controles tienen el error min. | — |
rejects a notification time that is not before the resolution time | Dado resolve 5 y notify 5; luego notify 4. | notifyBeforeResolveError es true con 5/5 y false con 4. | — |
does not flag the cross-field error while auto-resolution is disabled | Dado enabled: false y tiempos incoherentes (resolve 1, notify 9). | notifyBeforeResolveError es false. | — |
disables the dependent controls when auto-resolution is off | Dado enabled: false. | resolveInMinutes, notifyInMinutes, notificationMessage y sendFarewellMessage quedan deshabilitados; enabled sigue habilitado. | — |
re-enables the dependent controls when auto-resolution is turned on | Dado enabled: false y luego enabled pasa a true. | resolveInMinutes y notificationMessage se rehabilitan. | — |
Total del spec: 13 tests.
src/app/features/settings/components/business-hours-panel/business-hours-panel.component.spec.ts
- SUT:
BusinessHoursPanelComponent(horario de atención). - Mocking:
TestBedcon los mismos módulos yNO_ERRORS_SCHEMAque el panel anterior;SettingsServicespy conupdateScheduleyToastrNotificationServicespy. HelpersmakeBusinessHours(7 días) ymakeTenantSettings.
describe BusinessHoursPanelComponent
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
should build a 7-day FormArray from the settings input | Dado settings con 7 días. | daysArray.length es 7 y el primero es 'monday'. | — |
should populate the top-level fields from the settings input | Dado absenceMessage: 'Volvemos pronto'. | enabled es true y absenceMessage coincide. | — |
should call SettingsService.updateSchedule with the full payload (including days) on save | Dado un formulario cargado, cuando se llama onSave(). | updateSchedule recibe un payload con 7 days y enabled: true. | updateSchedule con of(...) |
should emit saved with the updated businessHours section after a successful save | Dado que el servicio devuelve absenceMessage: 'Nuevo mensaje'. | saved emite la sección businessHours actualizada. | savedSpy, updateSchedule |
should reset the form to the last saved snapshot on cancel | Dado absenceMessage modificado sin guardar, cuando se llama onCancel(). | El control vuelve a 'Original'. | — |
should set isSaving back to false when the save request fails | Dado que updateSchedule falla con Error('boom'). | isSaving es false. | updateSchedule con throwError |
Total del spec: 6 tests.
src/app/features/settings/components/csat-panel/csat-panel.component.spec.ts
- SUT:
CsatPanelComponent(encuesta de satisfacción). - Mocking:
TestBedanálogo a los paneles anteriores;SettingsServicespy conupdateCsat,ToastrNotificationServicespy. HelpersmakeCsatymakeTenantSettings.
describe CsatPanelComponent
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
should populate the form (including nested perRatingMessage) from the settings input | Dado perRatingMessage con rating1..rating5 = a..e. | enabled es true y perRatingMessage.rating5 es 'e'. | — |
should call SettingsService.updateCsat with the full nested payload on save | Dado perRatingMessage.rating1 cambiado a 'Lo lamentamos', cuando se guarda. | El payload incluye perRatingMessage.rating1 === 'Lo lamentamos'. | updateCsat con of(...) |
should emit saved with the updated csat section | Dado que el servicio devuelve thankYouMessage: 'Nuevo agradecimiento'. | saved emite la sección csat actualizada. | savedSpy, updateCsat |
should reset the form to the last saved snapshot on cancel | Dado thankYouMessage modificado sin guardar, cuando se llama onCancel(). | El control vuelve a 'Original'. | — |
should set isSaving back to false when the save request fails | Dado que updateCsat falla con Error('boom'). | isSaving es false. | updateCsat con throwError |
Total del spec: 5 tests.
src/app/features/settings/components/general-settings/general-settings.component.spec.ts
- SUT:
GeneralSettingsComponent(idioma y zona horaria). - Mocking:
TestBedconReactiveFormsModule,TranslateModule.forRoot(),NO_ERRORS_SCHEMA,SettingsServicespy (updateGeneral),ToastrNotificationServicespy yprovideMockStore(). Se espíaStore.dispatchconspyOn(store, 'dispatch').
describe GeneralSettingsComponent
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
should populate the form from the settings input | Dado settings { language: 'es', timezone: 'America/Bogota' }. | El formulario tiene es y America/Bogota. | — |
should call SettingsService.updateGeneral with the form value on save | Dado el formulario cambiado a en / UTC, cuando se guarda. | updateGeneral se llama con { language: 'en', timezone: 'UTC' }. | updateGeneral con of(...) |
should emit saved with the updated general section after a successful save | Dado que el servicio devuelve settings con general en/UTC. | saved emite updated.general. | savedSpy, updateGeneral |
should dispatch the language change to the store after a successful save | Dado un guardado exitoso con idioma en. | Se despacha change({ language: 'en' }). | spyOn(store, 'dispatch') |
should not dispatch the language change when the save request fails | Dado que updateGeneral falla con Error('boom') (ruta de fallo). | No se llama a dispatch. | updateGeneral con throwError, dispatch spy |
should reset the form to the last saved snapshot on cancel | Dado language cambiado a en sin guardar, cuando se llama onCancel(). | El control vuelve a 'es'. | — |
should set isSaving back to false when the save request fails | Dado que updateGeneral falla con Error('boom'). | isSaving es false. | updateGeneral con throwError |
Total del spec: 7 tests.
src/app/features/settings/pages/settings-page/settings-page.component.spec.ts
- SUT:
SettingsPageComponent(contenedor con pestañas). - Mocking:
TestBedconTranslateModule.forRoot(),NO_ERRORS_SCHEMAySettingsServicespy (getSettingsdevuelveof(settings)). HelpermakeTenantSettings().
describe SettingsPageComponent
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
should load settings via GET on init | Cuando se ejecuta detectChanges() (init). | Se llama getSettings y component.settings es igual a la respuesta. | getSettings con of(settings) |
should default the active tab to "general" | Tras la inicialización. | activeTab es 'general'. | getSettings |
should switch tabs via onTabChange | Cuando se llama onTabChange('attention'). | activeTab es 'attention'. | getSettings |
should set isLoading to false once settings are loaded | Tras la carga exitosa. | isLoading es false. | getSettings |
should update the general section in place when the general tab emits saved | Cuando se llama onGeneralSaved({ language: 'en', timezone: 'UTC' }). | settings.general se actualiza a esos valores. | getSettings |
Total del spec: 5 tests.
src/app/features/settings/settings-routing.module.spec.ts
- SUT: constante
routesdesettings-routing.module. - Mocking: ninguno; prueba estructural directa sobre la configuración de rutas (importa
permissionGuard,AuthGuard,AdminGuard).
describe SettingsRoutingModule routes (permission wiring)
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|---|---|---|
should gate the settings page route on SETTINGS_READ via permissionGuard, keeping existing guards | Dada la ruta con path === ''. | data es { permission: 'SETTINGS_READ' } y canActivate contiene permissionGuard, AuthGuard y AdminGuard. | — |
Total del spec: 1 test.
Total de la sección settings: 37 tests (6 specs).
Total de la página
11 specs y 90 tests (auth: 15, profile: 38, settings: 37).