Skip to main content

Componentes, directivas, pipes y rutas compartidas

Casos de prueba unitarios (Karma + Jasmine) de src/app/shared/**: componentes reutilizables, directivas estructurales de permisos, el pipe TruncatePipe y el cableado de guards en content.routes.ts. Son 13 specs y 102 tests. No hay xit, fit, fdescribe ni xdescribe, y ningún spec está vacío.

Los títulos de it() se transcriben tal cual están en el código (en inglés).

Componentes​

src/app/shared/components/audio-player/audio-player.component.spec.ts​

  • SUT: AudioPlayerComponent (reproductor de notas de voz con forma de onda).
  • Mocking: TestBed con declarations: [AudioPlayerComponent], sin providers. component.src fijo en https://cdn.example.com/nota.ogg. El elemento audio real se espía con spyOn o se le redefine duration con Object.defineProperty.

AudioPlayerComponent​

it()EscenarioResultado esperadoMocks/spies
paints the waveform that came with the fileDado waveform = [10, 50, 90], cuando se detectan cambiosSe renderizan 3 .audio-player__barNinguno
caps the bar count so the player fits a narrow bubbleDado un waveform de 64 muestras, cuando se renderizaSe renderizan exactamente 44 barras (tope)Ninguno
averages the samples it merges instead of dropping themDado 64 muestras (32 en 0 y 32 en 100), cuando se calculan las barrasbars[0] es 0 y la última barra es 100 (promedia, no descarta)Ninguno
falls back to a synthetic waveform when the file has noneDado waveform = null, cuando se renderizaHay al menos una barra (forma de onda sintética)Ninguno
cycles the playback rate 1x -> 1.5x -> 2x -> 1xDado rate inicial 1, cuando se llama cycleRate() tres vecesrate pasa por 1.5, 2 y vuelve a 1Ninguno
shows the stored duration before playingDado durationSeconds = 95 sin reproducirtimeLabel es 1:35Ninguno
marks bars as played up to the current progressDado waveform de 4 muestras y progress = 50barPlayed(0) y barPlayed(1) son true; barPlayed(3) es falseNinguno
stops playback when destroyed so a closed chat keeps quietDado el componente renderizado, cuando se llama ngOnDestroy()Se invoca pause sobre el elemento audiospyOn(audio, 'pause')
takes the duration from the file when the backend did not send oneDado durationSeconds = null y audio.duration = 6, cuando ocurre onLoadedMetadata()timeLabel es 0:06Object.defineProperty sobre duration
keeps the backend duration when the file already provided oneDado durationSeconds = 95 y audio.duration = 6, cuando ocurre onLoadedMetadata()durationSeconds sigue en 95 (no se sobrescribe)Object.defineProperty sobre duration

Tests en este archivo: 10.

src/app/shared/components/confirm-dialog/confirm-dialog.component.spec.ts​

  • SUT: ConfirmDialogComponent (modal de confirmación).
  • Mocking: TestBed con TranslateModule.forRoot(), NO_ERRORS_SCHEMA y NgbActiveModal reemplazado por jasmine.createSpyObj('NgbActiveModal', ['close', 'dismiss']).

ConfirmDialogComponent​

it()EscenarioResultado esperadoMocks/spies
keeps the icon layout for the existing callersDado icon = 'danger'isCompact es false, existe .confirm-dialog__icon-wrapper y no existe .confirm-dialog--compactNgbActiveModal spy
drops the icon in compact modeDado icon = 'none'isCompact es true, no hay icon-wrapper y sí existe .confirm-dialog--compactNgbActiveModal spy
paints the confirm button red when the action destroys somethingDado icon = 'none' y danger = trueExiste .confirm-dialog__confirm--dangerNgbActiveModal spy
closes with true on confirm and dismisses on cancelCuando se llaman onConfirm() y onCancel()close se llama con true y dismiss con falseactiveModal.close, activeModal.dismiss

Tests en este archivo: 4.

src/app/shared/components/connection-status-bar/connection-status-bar.component.spec.ts​

  • SUT: ConnectionStatusBarComponent (píldora de estado del socket con periodo de gracia de 2000 ms y confirmación de restauración de 2600 ms).
  • Mocking: TestBed con TranslateModule.forRoot y TranslateFakeLoader. El estado entra por un Subject<ConnectionBarStatus> asignado a component.status$. Todos los tests usan fakeAsync/tick. Varios cierran con authenticated y tick(RESTORED_MS) para vaciar timers pendientes.

ConnectionStatusBarComponent​

it()EscenarioResultado esperadoMocks/spies
stays hidden while the socket is authenticatedDado el estado authenticated y pasada la graciavisible es false y no hay .conn-pillSubject de estado, fakeAsync
does not flash during the normal startup transitionDado idle y connecting (500 ms cada uno) y luego authenticated, esperando gracia y restauradomode queda hidden (sin parpadeo en arranque normal)Subject de estado, fakeAsync
announces the reconnection attempt once the grace period passesDado connecting y pasados 2000 msmode es reconnecting, isRetryable false y no existe .conn-pill__retrySubject de estado, fakeAsync
offers a retry only once the connection is fully downDado idle y pasada la graciamode es down, la píldora tiene conn-pill--down y existe .conn-pill__retrySubject de estado, fakeAsync
switches from reconnecting to down without waiting againDado connecting ya visible y luego errormode pasa a down sin una segunda ventana de graciaSubject de estado, fakeAsync
emits retry when the button is clickedDado error pasada la gracia, cuando se hace clic en .conn-pill__retryEl output retry emite exactamente una vezSuscripción a retry, fakeAsync
confirms the recovery briefly, then retires itselfDado idle pasada la gracia y luego authenticatedmode es restored con conn-pill--restored; tras 2600 ms mode es hidden y no hay píldoraSubject de estado, fakeAsync
drops its timers on destroy so nothing fires afterwardsDado idle y se destruye el fixture antes de la graciaTras avanzar el tiempo mode sigue hidden (timers cancelados)fixture.destroy(), fakeAsync

Tests en este archivo: 8.

src/app/shared/components/date-time-picker/date-time-picker.component.spec.ts​

  • SUT: DateTimePickerComponent (standalone, selector de fecha y hora futura).
  • Mocking: TestBed con imports: [DateTimePickerComponent, TranslateModule.forRoot()], sin providers extra. Helper futureDateStruct(hoursFromNow) calcula fecha y hora futuras con Date.now() real. Los outputs se espían con jasmine.createSpy.

DateTimePickerComponent​

it()EscenarioResultado esperadoMocks/spies
defaults to one hour from now when no value input is providedDado el componente creado sin valuetime.hour es la hora actual + 1 (módulo 24)Ninguno
treats a past date/time as invalidDado date/time una hora en el pasadoisValidFutureSelection() es falseNinguno
treats a future date/time as validDado date/time una hora en el futuroisValidFutureSelection() es trueNinguno
emits validityChange(false) when the selection becomes invalidDado un suscriptor a validityChange, cuando se llaman onDateChange y onTimeChange con valores pasadosvalidityChange emite falsejasmine.createSpy('validityChange')
emits valueChange with the combined Date when the selection is validDado un suscriptor a valueChange, cuando se llaman onDateChange y onTimeChange con valores futurosEmite un Date cuyo año y hora coinciden con lo elegidojasmine.createSpy('valueChange')
adopts an externally-provided value on the FIRST ngOnChanges (prefill case, verify CRITICAL-4)Dado value externo y ngOnChanges con firstChange: truedate y time (hora y minuto) adoptan el valor externoSimpleChange construido a mano
adopts an externally-provided value on ngOnChangesDado value externo y ngOnChanges con firstChange: false y previousValue: nulldate y time (hora y minuto) adoptan el valor externoSimpleChange construido a mano

Tests en este archivo: 7.

src/app/shared/components/header/elements/my-account/my-account.component.spec.ts​

  • SUT: MyAccountComponent (avatar del navbar: foto o iniciales).
  • Mocking: TestBed con TranslateFakeLoader, NO_ERRORS_SCHEMA y providers stub: GlobalService (profile: of(profile)), NavService ({}), LayoutService ({ isDark: false }) y Store (jasmine.createSpyObj('Store', ['dispatch'])). Función configure(profile) que arma el módulo por test.

MyAccountComponent — avatar rendering​

it()EscenarioResultado esperadoMocks/spies
renders an <img> with the avatarUrl when the profile has oneDado un perfil con avatarUrlExiste img.profile-avatar-photo con src igual a la URL y no existe .profile-avatar-fallbackStubs de GlobalService, Store
renders the initials fallback when the profile has no avatarUrlDado un perfil sin avatarUrlNo hay img.profile-avatar-photo y sí existe .profile-avatar-fallbackStubs de GlobalService, Store

Tests en este archivo: 2.

src/app/shared/components/layout/content/content.component.spec.ts​

  • SUT: ContentComponent (layout principal que conecta los sockets).
  • Mocking: TestBed con el template reemplazado por <div></div> (overrideComponent). Providers: ActivatedRoute, LayoutService, NavService, IframeDetectionService y Store como stubs; GlobalService.profile es un ReplaySubject<User>; InternalChatSocketService y ChatSocketService con jasmine.createSpyObj(..., ['connect', 'disconnect']).

ContentComponent > ngOnInit​

it()EscenarioResultado esperadoMocks/spies
connects the internal-chat and chat sockets once a user profile with an id arrivesDado ngOnInit() y un perfil con _id: 'user-42'Ambos sockets llaman connect('user-42')Spies de ambos sockets
connects both sockets again with a different id for a different profile emissionDado ngOnInit() y un perfil con _id: 'user-99'Ambos sockets llaman connect('user-99')Spies de ambos sockets
does not connect either socket while the profile has no _id yetDado ngOnInit() y un perfil con _id vacíoNingún socket llama connectSpies de ambos sockets

ContentComponent > ngOnDestroy​

it()EscenarioResultado esperadoMocks/spies
disconnects both the internal-chat and chat socketsCuando se llama ngOnDestroy()Ambos sockets llaman disconnectSpies de ambos sockets

Tests en este archivo: 4.

src/app/shared/components/rich-text-editor/rich-text-editor.component.spec.ts​

  • SUT: RichTextEditorComponent (editor tipo WhatsApp que implementa ControlValueAccessor).
  • Mocking: TestBed con declarations: [RichTextEditorComponent] e imports: [ReactiveFormsModule], sin providers. Callbacks con jasmine.createSpy; en un bloque se usa un FormControl real. applyFormat usa el textarea real con selectionStart/selectionEnd.

RichTextEditorComponent > ControlValueAccessor​

it()EscenarioResultado esperadoMocks/spies
writeValue should set the internal valueCuando writeValue('hello world')value es hello worldNinguno
writeValue should default to an empty string for null/undefinedCuando writeValue(null)value es cadena vacíaNinguno
registerOnChange callback should be invoked on user inputDado un callback registrado, cuando onInput('new text')El callback se llama con new textjasmine.createSpy('onChange')
registerOnTouched callback should be invoked on user inputDado un callback registrado, cuando onInput('typed')El callback de touched se invocajasmine.createSpy('onTouched')
setDisabledState should toggle the disabled flagCuando setDisabledState(true) y luego (false)disabled pasa a true y luego a falseNinguno
setDisabledState(true) should close an open emoji pickerDado showEmojiPicker = true, cuando setDisabledState(true)showEmojiPicker queda falseNinguno

RichTextEditorComponent > with a real FormControl​

it()EscenarioResultado esperadoMocks/spies
should sync value from control -> component via writeValueDado un FormControl('initial value') y writeValue(control.value)component.value es initial valueFormControl real
should propagate typed input back to the controlDado un FormControl('') enlazado con registerOnChange, cuando onInput('typed by user')control.value es typed by userFormControl real

RichTextEditorComponent > applyFormat​

it()EscenarioResultado esperadoMocks/spies
should wrap the selected text with the bold marker (*)Dado hello world con selección 0 a 5value es *hello* worldtextarea real
should wrap the selected text with the italic marker (_)Dado hello world con selección 6 a 11value es hello _world_textarea real
should wrap the selected text with the strikethrough marker (~)Dado hello world con selección 0 a 11value es ~hello world~textarea real
should do nothing when the component is disabledDado el componente deshabilitado y selección 0 a 5, cuando applyFormat('*')value no cambia (hello world)textarea real
should notify the registered onChange callback after applying a formatDado un onChange registrado y selección 0 a 5El callback se llama con *hello* worldjasmine.createSpy('onChange')

RichTextEditorComponent > toggleEmojiPicker​

it()EscenarioResultado esperadoMocks/spies
should toggle showEmojiPicker on clickDado un MouseEvent, cuando se llama toggleEmojiPicker dos vecesshowEmojiPicker pasa a true y luego a false; se llama stopPropagationspyOn(event, 'stopPropagation')
should NOT open when the component is disabledDado el componente deshabilitado, cuando toggleEmojiPickershowEmojiPicker sigue falseNinguno

RichTextEditorComponent > template​

it()EscenarioResultado esperadoMocks/spies
should render a textarea with the current valueDado writeValue('rendered text')El textarea muestra rendered textNinguno
should render 3 format tool buttons (bold, italic, strikethrough) and 1 emoji buttonDado el componente renderizadoHay 4 .rich-text-editor__toolNinguno
should disable the textarea when the control is disabledDado setDisabledState(true) y rendertextarea.disabled es trueNinguno

Tests en este archivo: 18.

src/app/shared/components/sidebar/sidebar.component.permission-gating.spec.ts​

  • SUT: SidebarComponent con las directivas reales ShowWithPermissionDirective y ShowForUserTypesDirective (regresión del desfase id/acrónimo del menú, fase 8.3). A diferencia de sidebar.component.spec.ts, aquí sí se declara la directiva real.
  • Mocking: TestBed con TranslateModule.forRoot() y NO_ERRORS_SCHEMA. Providers: NavService stub con BehaviorSubject<Menu[]>, LayoutService stub, Router stub (events), GlobalService (profile: of({ userType: 'admin' })) y MyPermissionsService con jasmine.createSpyObj('MyPermissionsService', ['hasPermission', 'hasAnyPermission']). La función setup(menuItems) configura y renderiza.

SidebarComponent — permission gating (sidebar acronym-gap fix)​

it()EscenarioResultado esperadoMocks/spies
should HIDE a menu item with a permission field when the acronym is NOT grantedDado un ítem contacts con permission: 'CONTACT_READ' y hasPermission devolviendo of(false)Se consulta hasPermission('CONTACT_READ') y el texto sidebarMenu.contacts no aparecehasPermission.and.returnValue(of(false))
should SHOW a menu item with a permission field when the acronym IS grantedMismo ítem con hasPermission devolviendo of(true)El texto sidebarMenu.contacts aparecehasPermission.and.returnValue(of(true))
should ALWAYS SHOW a menu item with no permission field, even when the permission service denies everything (dashboard case)Dado un ítem dashboard sin permission y el servicio denegando todoEl texto dashboard.titleSidebar aparece y hasPermission nunca se llamahasPermission y hasAnyPermission devuelven of(false)
should gate a second-level (children) item on its own permission field, independent of its ungated parent containerDado un padre sub sin permiso con un hijo con TICKET_READ y otro sin permiso; hasPermission concede solo TICKET_READAparecen el padre, el hijo con permiso y el hijo sin permisohasPermission.and.callFake

Tests en este archivo: 4.

src/app/shared/components/sidebar/sidebar.component.spec.ts​

  • SUT: SidebarComponent (menú lateral, navegación y scroll horizontal).
  • Mocking: TestBed con TranslateModule.forRoot() y NO_ERRORS_SCHEMA (no declara las directivas reales). Providers: NavService stub con BehaviorSubject<Menu[]> y collapseSidebar, LayoutService stub y Router stub con events desde un Subject (se emiten NavigationEnd a mano). Fixtures ITEM_A (link), ITEM_B (sub de 2 niveles) e ITEM_C (sub de 3 niveles) y helper makeItem.

SidebarComponent > initialization​

it()EscenarioResultado esperadoMocks/spies
should create the componentDado el componente creadoEs truthyStubs de servicios
should populate menuItems from NavService.itemsDado el BehaviorSubject inicial de 3 ítemsmenuItems.length es 3BehaviorSubject
should update menuItems when NavService.items emits a new valueCuando items emite un solo ítem zmenuItems tiene 1 elemento con id zBehaviorSubject

SidebarComponent > router navigation​

it()EscenarioResultado esperadoMocks/spies
should activate a top-level item when its path matches NavigationEnd urlDado NavigationEnd con /homeEl ítem a queda activeSubject de eventos del router
should activate a second-level item when its path matches NavigationEnd urlDado NavigationEnd con /playersEl padre b queda activeSubject de eventos del router
should activate a third-level item when its path matches NavigationEnd urlDado NavigationEnd con /reports/summaryEl padre c queda activeSubject de eventos del router

SidebarComponent > sidebarToggle​

it()EscenarioResultado esperadoMocks/spies
should set collapseSidebar to true when it was falseDado collapseSidebar = false, cuando sidebarToggle()collapseSidebar es truenavService stub
should set collapseSidebar to false when it was trueDado collapseSidebar = true, cuando sidebarToggle()collapseSidebar es falsenavService stub

SidebarComponent > setNavActive​

it()EscenarioResultado esperadoMocks/spies
should deactivate all other top-level items when one is activatedDado dos ítems activos, cuando setNavActive(items[0])items[1].active pasa a falseNinguno
should activate the parent item when a direct child is the targetCuando setNavActive(hijo)El padre queda activeNinguno
should activate parent and sub-parent when a grandchild is the targetCuando setNavActive(nieto)Padre y subpadre quedan activeNinguno

SidebarComponent > toggletNavActive​

it()EscenarioResultado esperadoMocks/spies
should set item.active to true when it was falseDado un ítem inactivoQueda active trueNinguno
should set item.active to false when it was trueDado un ítem activoQueda active falseNinguno
should deactivate sibling items when activating a top-level itemDado items[1] activo e items[0] inactivo, cuando toggletNavActive(items[0])items[0] activo y items[1] inactivoNinguno

SidebarComponent > hasChildrenWithPath​

it()EscenarioResultado esperadoMocks/spies
should return true when at least one child has a pathDado un hijo con pathDevuelve trueNinguno
should return false when no child has a pathDado un hijo con path indefinidoDevuelve falseNinguno
should return false when children is undefinedDado un ítem sin childrenDevuelve falseNinguno

SidebarComponent > scrollToLeft​

it()EscenarioResultado esperadoMocks/spies
should reset margin to 0 and set leftArrowNone when margin >= -widthDado margin = -100 y width = 200margin 0, leftArrowNone true y rightArrowNone falseNinguno
should increment margin by width when margin < -widthDado margin = -600 y width = 200margin -400 y rightArrowNone falseNinguno

SidebarComponent > scrollToRight​

it()EscenarioResultado esperadoMocks/spies
should decrement margin by widthDado margin = 0 y width = 200margin -200 y leftArrowNone falseNinguno
should cap margin at -3464 and set rightArrowNone when margin <= -3051Dado margin = -3051 y width = 200 (borde del tope)margin -3464, rightArrowNone true y leftArrowNone falseNinguno

SidebarComponent > onResize​

it()EscenarioResultado esperadoMocks/spies
should update width from the resize eventDado un evento con innerWidth: 1440width es 940Evento simulado

Tests en este archivo: 22.

Directivas​

src/app/shared/directives/show-for-user-types.directive.spec.ts​

  • SUT: ShowForUserTypesDirective (selector *appHideForUserTypes, oculta el contenido para los tipos de usuario denegados).
  • Mocking: TestBed con HostComponent de prueba y la directiva declarada; GlobalService stub con profile como ReplaySubject<User> (helper emitUser(userType)).

ShowForUserTypesDirective​

it()EscenarioResultado esperadoMocks/spies
should hide the content for a userType in the denied listDado denied = ['atc'] y usuario atcNo existe .protectedReplaySubject de perfil
should show the content for a userType NOT in the denied listDado denied = ['atc'] y usuario agentExiste .protectedReplaySubject de perfil
should ALWAYS show the content for admin, even if admin were in the denied listDado denied = ['admin'] y usuario adminExiste .protected (admin siempre ve)ReplaySubject de perfil
should show the content when the denied list is emptyDado denied = [] y usuario atcExiste .protectedReplaySubject de perfil
should fall back to the default hide list when no value is providedDado denied = undefined y usuario agentNo existe .protected (lista por defecto)ReplaySubject de perfil
should update visibility when the profile userType changesDado primero atc y luego adminPrimero oculto, luego visibleReplaySubject de perfil

Tests en este archivo: 6.

src/app/shared/directives/show-with-permission.directive.spec.ts​

  • SUT: ShowWithPermissionDirective (selector *appShowWithPermission, con rama else opcional).
  • Mocking: TestBed con dos hosts de prueba (HostComponent y HostWithElseComponent) y MyPermissionsService como jasmine.createSpyObj('MyPermissionsService', ['hasPermission', 'hasAnyPermission']). Helper setup(componentType).

ShowWithPermissionDirective​

it()EscenarioResultado esperadoMocks/spies
should render the content when the acronym is grantedDado hasPermission devolviendo of(true)Existe .protectedhasPermission
should hide the content when the acronym is NOT grantedDado hasPermission devolviendo of(false)No existe .protectedhasPermission
should default to hidden while the permission check is still pending (deny-by-default)Dado hasPermission devolviendo un Subject que no emiteNo existe .protected (denegado por defecto)Subject<boolean> pendiente
should render the else template when denied and providedDado hasPermission of(false) y host con else deniedNo existe .protected y sí existe .fallbackhasPermission
should render when ANY acronym in the array is grantedDado required = ['CONTACT_READ', 'CONTACT_UPDATE'] y hasAnyPermission of(true)Se llama hasAnyPermission con el array y existe .protectedhasAnyPermission

Tests en este archivo: 5.

Pipes​

src/app/shared/pipes/truncate.pipe.spec.ts​

  • SUT: TruncatePipe (maxLength por defecto 100, sufijo …).
  • Mocking: sin TestBed: new TruncatePipe() directo en beforeEach. Sin spies.

TruncatePipe​

it()EscenarioResultado esperadoMocks/spies
should return empty string for nullDado nullDevuelve ''Ninguno
should return empty string for undefinedDado undefinedDevuelve ''Ninguno
should return empty string for empty stringDado ''Devuelve ''Ninguno
should return the original string when shorter than maxLengthDado Hello con maxLength 10Devuelve HelloNinguno
should return the original string when equal to maxLengthDado Hello con maxLength 5 (borde)Devuelve HelloNinguno
should truncate and append ellipsis when longer than maxLengthDado Hello World con maxLength 5Devuelve Hello…Ninguno
should use default maxLength of 100Dado cadenas de 100 y 101 caracteres sin maxLengthLa de 100 queda igual y la de 101 se corta a 100 más …Ninguno
should truncate correctly with maxLength 0Dado abc con maxLength 0Devuelve …Ninguno

Tests en este archivo: 8.

Rutas​

src/app/shared/routes/content.routes.spec.ts​

  • SUT: arreglo content de content.routes.ts (cableado de permissionGuard).
  • Mocking: sin TestBed ni spies: se importan content, permissionGuard y PAGE_URLS y se inspeccionan las rutas (data y canActivate).

content routes (permission wiring)​

it()EscenarioResultado esperadoMocks/spies
should gate CONTACTS on CONTACT_READ via permissionGuardDada la ruta PAGE_URLS.CONTACTSdata es { permission: 'CONTACT_READ' } y canActivate contiene permissionGuardNinguno
should gate SETTINGS on SETTINGS_READ via permissionGuardDada la ruta PAGE_URLS.SETTINGSdata es { permission: 'SETTINGS_READ' } y canActivate contiene permissionGuardNinguno
should leave DASHBOARD ungated (no data.permission, no permissionGuard)Dada la ruta PAGE_URLS.DASHBOARDdata es undefined y canActivate no contiene permissionGuardNinguno
should leave PROFILE ungated (self-service, no data.permission, no permissionGuard)Dada la ruta PAGE_URLS.PROFILEdata es undefined y canActivate no contiene permissionGuardNinguno

Tests en este archivo: 4.

Total de la página​

13 specs y 102 tests (10 + 4 + 8 + 7 + 2 + 4 + 18 + 4 + 22 + 6 + 5 + 8 + 4).