Skip to main content

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: TestBed con declarations: [LoginComponent], ReactiveFormsModule, TranslateModule.forRoot con TranslateFakeLoader, provideMockStore({ initialState }) (estado auth con user: null, loading: false, message: null) y provideMockActions alimentado por un Subject<Action> (actionsSubject) que se completa en afterEach. Se espía store.dispatch con spyOn.

describe LoginComponent​

it()EscenarioResultado esperadoMocks/spies
should createDado el componente creado con detectChanges, cuando se inspecciona la instancia.La instancia existe.MockStore, provideMockActions
should build a form with email and password controlsDado el componente inicializado, cuando se consultan los controles.Existen los controles email y password.—
should start with an invalid formDado el formulario recién construido (campos vacíos).El formulario es inválido.—
should start with showPassword = falseDado el componente recién creado.showPassword es false.—
should be invalid with a non-email valueCuando 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 emptyCuando 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 passwordCuando se asigna user@example.com y secret.El formulario completo es válido.—
should NOT dispatch when the form is invalidDado 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 submitDado 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 submittedDado 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 callDado showPassword en false, cuando se invoca togglePassword() dos veces.Pasa a true y vuelve a false.—
should emit the error message when userAuthenticationFailure firesCuando 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 errorsCuando 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: TestBed con FormsModule, TranslateModule.forRoot() e ImageCropperComponent importado; NgbActiveModal reemplazado por jasmine.createSpyObj('NgbActiveModal', ['close', 'dismiss']). Se crea el componente sin fixture.detectChanges() para no renderizar el cropper real; el evento de recorte se construye a mano (ImageCroppedEvent) y se pasa a onImageCropped(). imageFile es un File JPG sintético.

describe onZoomChange (D4 — zoom slider clamped to [1,3], new transform object)​

it()EscenarioResultado esperadoMocks/spies
clamps a value above MAX_SCALE down to 3 and reassigns transform with a NEW objectCuando 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()EscenarioResultado esperadoMocks/spies
keeps the drag translate values from the (transformChange) outputCuando 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()EscenarioResultado esperadoMocks/spies
does NOT close the modal when no cropped blob is available yetDado 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 blobDado 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 blobDado 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()EscenarioResultado esperadoMocks/spies
dismisses the modal with nullCuando 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) con jasmine.createSpyObj('NgbModal', ['open']) que devuelve una NgbModalRef falsa (componentInstance y result). No se renderiza ningún modal.

describe AvatarCropperDialogService​

it()EscenarioResultado esperadoMocks/spies
passes the file to componentInstance.imageFileDado 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 resolvesDado 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étodo onAvatarSelected).
  • Mocking: instanciación directa new ProfilePageComponent(new FormBuilder(), store, userService, notification, new UserAvatarsService(), avatarCropperDialogService), con jasmine.createSpyObj para Store (select devuelve of(currentUser), dispatch), UserService (updateMySignature, updateMyAvatar), ToastrNotificationService y AvatarCropperDialogService (open). Se llama ngOnInit() en beforeEach y ngOnDestroy() en afterEach. El archivo se simula con un input[type=file] y DataTransfer.

describe ProfilePageComponent — avatar upload (with crop dialog)​

describe reject non-JPG file (client-side mirror of backend validation)​
it()EscenarioResultado esperadoMocks/spies
never opens the crop dialog nor calls updateMyAvatar for a PNGDado 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()EscenarioResultado esperadoMocks/spies
never opens the crop dialog nor calls updateMyAvatar for a file over the 2MB capDado 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()EscenarioResultado esperadoMocks/spies
does not upload, leaves avatarPreviewUrl unchanged, and does not dispatch when the dialog resolves nullDado 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()EscenarioResultado esperadoMocks/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 resolvesDado un recorte confirmado y subida exitosa.isUploadingAvatar es false.open, updateMyAvatar
describe upload failure after confirm​
it()EscenarioResultado esperadoMocks/spies
revokes the preview and surfaces an error without dispatchingDado 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()EscenarioResultado esperadoMocks/spies
is a no-opDado 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). StarredMessageService es un jasmine.createSpyObj con método findGlobal y propiedad changed$ (un Subject expuesto como observable); Router con navigate; ToastrNotificationService con showNotification. El helper listItem(overrides) construye un StarredMessageListItem.

describe load​

it()EscenarioResultado esperadoMocks/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 errorDado que findGlobal falla con Error('boom').items queda vacío e isLoading es false.findGlobal con throwError

describe refresh after a toggle elsewhere​

it()EscenarioResultado esperadoMocks/spies
reloads the list from page 1 when a message is starred or un-starredDado 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 destroyedDado ngOnInit() y luego ngOnDestroy(), cuando changed$ emite.findGlobal no se llama.changed$, findGlobal

describe setFilter​

it()EscenarioResultado esperadoMocks/spies
reloads page 1 scoped to the chosen messageTypeCuando 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 filterDado activeFilter = 'contact', cuando se vuelve a seleccionar 'contact'.findGlobal no se llama.findGlobal

describe getPreview​

it()EscenarioResultado esperadoMocks/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 filenameDado un item con archivo sin filename.Devuelve '[archivo]'.—
renders the raw body when there is no attached fileDado un item sin archivo y message.body = 'texto'.Devuelve 'texto'.—

describe getSenderName​

it()EscenarioResultado esperadoMocks/spies
returns firstname + lastName when both are presentDado un remitente con firstname: 'Alan' y lastName: 'Turing'.Devuelve 'Alan Turing'.—
falls back to username when firstname/lastName are missingDado un remitente solo con username: 'alan'.Devuelve 'alan'.—
returns an empty string when sender is undefinedDado sender: undefined.Devuelve ''.—

describe hasFile / isFileImage / getFileIconClass​

it()EscenarioResultado esperadoMocks/spies
reports no file when file is nullDado 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 mimetypeDado un archivo con mimetype: 'image/webp'.isFileImage es true.—

describe openItem — click branching (task 4.5)​

it()EscenarioResultado esperadoMocks/spies
navigates to whaticket chat with the ticketId + highlightMessageId for a contact itemDado 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 itemDado 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 conversationIdDado 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: TestBed con ReactiveFormsModule, TranslateModule.forRoot(), SwitchModule, RichTextEditorModule y NO_ERRORS_SCHEMA; SettingsService como jasmine.createSpyObj con updateAutoResolution y ToastrNotificationService con showNotification. Helpers makeAutoResolution(overrides) y makeTenantSettings(autoResolution). La entrada settings se asigna antes de fixture.detectChanges().

describe AutoResolutionPanelComponent​

it()EscenarioResultado esperadoMocks/spies
should populate the form from the settings inputDado settings con resolveInMinutes: 15 y enabled: true.El formulario refleja 15 y true.—
should not expose a farewellMessage controlDado un settings válido.form.get('farewellMessage') es null.—
should call SettingsService.updateAutoResolution with the form payload on saveDado 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 textDado sendFarewellMessage: false, cuando se guarda.El payload lleva sendFarewellMessage: false y no contiene farewellMessage.updateAutoResolution con of(...)
should emit saved with the updated autoResolution sectionDado 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 cancelDado 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 failsDado que updateAutoResolution falla con Error('boom') (ruta de fallo).isSaving es false.updateAutoResolution con throwError
describe validations​
it()EscenarioResultado esperadoMocks/spies
requires resolveInMinutesDado resolveInMinutes en null.El control tiene el error required y el formulario es inválido.—
rejects times below one minuteDado resolveInMinutes: 0 y notifyInMinutes: 0.Ambos controles tienen el error min.—
rejects a notification time that is not before the resolution timeDado 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 disabledDado enabled: false y tiempos incoherentes (resolve 1, notify 9).notifyBeforeResolveError es false.—
disables the dependent controls when auto-resolution is offDado enabled: false.resolveInMinutes, notifyInMinutes, notificationMessage y sendFarewellMessage quedan deshabilitados; enabled sigue habilitado.—
re-enables the dependent controls when auto-resolution is turned onDado 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: TestBed con los mismos módulos y NO_ERRORS_SCHEMA que el panel anterior; SettingsService spy con updateSchedule y ToastrNotificationService spy. Helpers makeBusinessHours (7 días) y makeTenantSettings.

describe BusinessHoursPanelComponent​

it()EscenarioResultado esperadoMocks/spies
should build a 7-day FormArray from the settings inputDado settings con 7 días.daysArray.length es 7 y el primero es 'monday'.—
should populate the top-level fields from the settings inputDado absenceMessage: 'Volvemos pronto'.enabled es true y absenceMessage coincide.—
should call SettingsService.updateSchedule with the full payload (including days) on saveDado 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 saveDado 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 cancelDado absenceMessage modificado sin guardar, cuando se llama onCancel().El control vuelve a 'Original'.—
should set isSaving back to false when the save request failsDado 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: TestBed análogo a los paneles anteriores; SettingsService spy con updateCsat, ToastrNotificationService spy. Helpers makeCsat y makeTenantSettings.

describe CsatPanelComponent​

it()EscenarioResultado esperadoMocks/spies
should populate the form (including nested perRatingMessage) from the settings inputDado perRatingMessage con rating1..rating5 = a..e.enabled es true y perRatingMessage.rating5 es 'e'.—
should call SettingsService.updateCsat with the full nested payload on saveDado 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 sectionDado 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 cancelDado thankYouMessage modificado sin guardar, cuando se llama onCancel().El control vuelve a 'Original'.—
should set isSaving back to false when the save request failsDado 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: TestBed con ReactiveFormsModule, TranslateModule.forRoot(), NO_ERRORS_SCHEMA, SettingsService spy (updateGeneral), ToastrNotificationService spy y provideMockStore(). Se espía Store.dispatch con spyOn(store, 'dispatch').

describe GeneralSettingsComponent​

it()EscenarioResultado esperadoMocks/spies
should populate the form from the settings inputDado settings { language: 'es', timezone: 'America/Bogota' }.El formulario tiene es y America/Bogota.—
should call SettingsService.updateGeneral with the form value on saveDado 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 saveDado 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 saveDado un guardado exitoso con idioma en.Se despacha change({ language: 'en' }).spyOn(store, 'dispatch')
should not dispatch the language change when the save request failsDado 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 cancelDado 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 failsDado 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: TestBed con TranslateModule.forRoot(), NO_ERRORS_SCHEMA y SettingsService spy (getSettings devuelve of(settings)). Helper makeTenantSettings().

describe SettingsPageComponent​

it()EscenarioResultado esperadoMocks/spies
should load settings via GET on initCuando 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 onTabChangeCuando se llama onTabChange('attention').activeTab es 'attention'.getSettings
should set isLoading to false once settings are loadedTras la carga exitosa.isLoading es false.getSettings
should update the general section in place when the general tab emits savedCuando 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 routes de settings-routing.module.
  • Mocking: ninguno; prueba estructural directa sobre la configuración de rutas (importa permissionGuard, AuthGuard, AdminGuard).

describe SettingsRoutingModule routes (permission wiring)​

it()EscenarioResultado esperadoMocks/spies
should gate the settings page route on SETTINGS_READ via permissionGuard, keeping existing guardsDada 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).