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() | Escenario | Resultado esperado | Mocks/spies |
|---|
paints the waveform that came with the file | Dado waveform = [10, 50, 90], cuando se detectan cambios | Se renderizan 3 .audio-player__bar | Ninguno |
caps the bar count so the player fits a narrow bubble | Dado un waveform de 64 muestras, cuando se renderiza | Se renderizan exactamente 44 barras (tope) | Ninguno |
averages the samples it merges instead of dropping them | Dado 64 muestras (32 en 0 y 32 en 100), cuando se calculan las barras | bars[0] es 0 y la última barra es 100 (promedia, no descarta) | Ninguno |
falls back to a synthetic waveform when the file has none | Dado waveform = null, cuando se renderiza | Hay al menos una barra (forma de onda sintética) | Ninguno |
cycles the playback rate 1x -> 1.5x -> 2x -> 1x | Dado rate inicial 1, cuando se llama cycleRate() tres veces | rate pasa por 1.5, 2 y vuelve a 1 | Ninguno |
shows the stored duration before playing | Dado durationSeconds = 95 sin reproducir | timeLabel es 1:35 | Ninguno |
marks bars as played up to the current progress | Dado waveform de 4 muestras y progress = 50 | barPlayed(0) y barPlayed(1) son true; barPlayed(3) es false | Ninguno |
stops playback when destroyed so a closed chat keeps quiet | Dado el componente renderizado, cuando se llama ngOnDestroy() | Se invoca pause sobre el elemento audio | spyOn(audio, 'pause') |
takes the duration from the file when the backend did not send one | Dado durationSeconds = null y audio.duration = 6, cuando ocurre onLoadedMetadata() | timeLabel es 0:06 | Object.defineProperty sobre duration |
keeps the backend duration when the file already provided one | Dado 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() | Escenario | Resultado esperado | Mocks/spies |
|---|
keeps the icon layout for the existing callers | Dado icon = 'danger' | isCompact es false, existe .confirm-dialog__icon-wrapper y no existe .confirm-dialog--compact | NgbActiveModal spy |
drops the icon in compact mode | Dado icon = 'none' | isCompact es true, no hay icon-wrapper y sí existe .confirm-dialog--compact | NgbActiveModal spy |
paints the confirm button red when the action destroys something | Dado icon = 'none' y danger = true | Existe .confirm-dialog__confirm--danger | NgbActiveModal spy |
closes with true on confirm and dismisses on cancel | Cuando se llaman onConfirm() y onCancel() | close se llama con true y dismiss con false | activeModal.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() | Escenario | Resultado esperado | Mocks/spies |
|---|
stays hidden while the socket is authenticated | Dado el estado authenticated y pasada la gracia | visible es false y no hay .conn-pill | Subject de estado, fakeAsync |
does not flash during the normal startup transition | Dado idle y connecting (500 ms cada uno) y luego authenticated, esperando gracia y restaurado | mode queda hidden (sin parpadeo en arranque normal) | Subject de estado, fakeAsync |
announces the reconnection attempt once the grace period passes | Dado connecting y pasados 2000 ms | mode es reconnecting, isRetryable false y no existe .conn-pill__retry | Subject de estado, fakeAsync |
offers a retry only once the connection is fully down | Dado idle y pasada la gracia | mode es down, la píldora tiene conn-pill--down y existe .conn-pill__retry | Subject de estado, fakeAsync |
switches from reconnecting to down without waiting again | Dado connecting ya visible y luego error | mode pasa a down sin una segunda ventana de gracia | Subject de estado, fakeAsync |
emits retry when the button is clicked | Dado error pasada la gracia, cuando se hace clic en .conn-pill__retry | El output retry emite exactamente una vez | Suscripción a retry, fakeAsync |
confirms the recovery briefly, then retires itself | Dado idle pasada la gracia y luego authenticated | mode es restored con conn-pill--restored; tras 2600 ms mode es hidden y no hay píldora | Subject de estado, fakeAsync |
drops its timers on destroy so nothing fires afterwards | Dado idle y se destruye el fixture antes de la gracia | Tras 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() | Escenario | Resultado esperado | Mocks/spies |
|---|
defaults to one hour from now when no value input is provided | Dado el componente creado sin value | time.hour es la hora actual + 1 (módulo 24) | Ninguno |
treats a past date/time as invalid | Dado date/time una hora en el pasado | isValidFutureSelection() es false | Ninguno |
treats a future date/time as valid | Dado date/time una hora en el futuro | isValidFutureSelection() es true | Ninguno |
emits validityChange(false) when the selection becomes invalid | Dado un suscriptor a validityChange, cuando se llaman onDateChange y onTimeChange con valores pasados | validityChange emite false | jasmine.createSpy('validityChange') |
emits valueChange with the combined Date when the selection is valid | Dado un suscriptor a valueChange, cuando se llaman onDateChange y onTimeChange con valores futuros | Emite un Date cuyo año y hora coinciden con lo elegido | jasmine.createSpy('valueChange') |
adopts an externally-provided value on the FIRST ngOnChanges (prefill case, verify CRITICAL-4) | Dado value externo y ngOnChanges con firstChange: true | date y time (hora y minuto) adoptan el valor externo | SimpleChange construido a mano |
adopts an externally-provided value on ngOnChanges | Dado value externo y ngOnChanges con firstChange: false y previousValue: null | date y time (hora y minuto) adoptan el valor externo | SimpleChange construido a mano |
Tests en este archivo: 7.
- 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() | Escenario | Resultado esperado | Mocks/spies |
|---|
renders an <img> with the avatarUrl when the profile has one | Dado un perfil con avatarUrl | Existe img.profile-avatar-photo con src igual a la URL y no existe .profile-avatar-fallback | Stubs de GlobalService, Store |
renders the initials fallback when the profile has no avatarUrl | Dado un perfil sin avatarUrl | No hay img.profile-avatar-photo y sí existe .profile-avatar-fallback | Stubs 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() | Escenario | Resultado esperado | Mocks/spies |
|---|
connects the internal-chat and chat sockets once a user profile with an id arrives | Dado 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 emission | Dado 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 yet | Dado ngOnInit() y un perfil con _id vacío | Ningún socket llama connect | Spies de ambos sockets |
ContentComponent > ngOnDestroy
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
disconnects both the internal-chat and chat sockets | Cuando se llama ngOnDestroy() | Ambos sockets llaman disconnect | Spies 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() | Escenario | Resultado esperado | Mocks/spies |
|---|
writeValue should set the internal value | Cuando writeValue('hello world') | value es hello world | Ninguno |
writeValue should default to an empty string for null/undefined | Cuando writeValue(null) | value es cadena vacía | Ninguno |
registerOnChange callback should be invoked on user input | Dado un callback registrado, cuando onInput('new text') | El callback se llama con new text | jasmine.createSpy('onChange') |
registerOnTouched callback should be invoked on user input | Dado un callback registrado, cuando onInput('typed') | El callback de touched se invoca | jasmine.createSpy('onTouched') |
setDisabledState should toggle the disabled flag | Cuando setDisabledState(true) y luego (false) | disabled pasa a true y luego a false | Ninguno |
setDisabledState(true) should close an open emoji picker | Dado showEmojiPicker = true, cuando setDisabledState(true) | showEmojiPicker queda false | Ninguno |
RichTextEditorComponent > with a real FormControl
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should sync value from control -> component via writeValue | Dado un FormControl('initial value') y writeValue(control.value) | component.value es initial value | FormControl real |
should propagate typed input back to the control | Dado un FormControl('') enlazado con registerOnChange, cuando onInput('typed by user') | control.value es typed by user | FormControl real |
RichTextEditorComponent > applyFormat
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should wrap the selected text with the bold marker (*) | Dado hello world con selección 0 a 5 | value es *hello* world | textarea real |
should wrap the selected text with the italic marker (_) | Dado hello world con selección 6 a 11 | value es hello _world_ | textarea real |
should wrap the selected text with the strikethrough marker (~) | Dado hello world con selección 0 a 11 | value es ~hello world~ | textarea real |
should do nothing when the component is disabled | Dado 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 format | Dado un onChange registrado y selección 0 a 5 | El callback se llama con *hello* world | jasmine.createSpy('onChange') |
RichTextEditorComponent > toggleEmojiPicker
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should toggle showEmojiPicker on click | Dado un MouseEvent, cuando se llama toggleEmojiPicker dos veces | showEmojiPicker pasa a true y luego a false; se llama stopPropagation | spyOn(event, 'stopPropagation') |
should NOT open when the component is disabled | Dado el componente deshabilitado, cuando toggleEmojiPicker | showEmojiPicker sigue false | Ninguno |
RichTextEditorComponent > template
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should render a textarea with the current value | Dado writeValue('rendered text') | El textarea muestra rendered text | Ninguno |
should render 3 format tool buttons (bold, italic, strikethrough) and 1 emoji button | Dado el componente renderizado | Hay 4 .rich-text-editor__tool | Ninguno |
should disable the textarea when the control is disabled | Dado setDisabledState(true) y render | textarea.disabled es true | Ninguno |
Tests en este archivo: 18.
- 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.
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should HIDE a menu item with a permission field when the acronym is NOT granted | Dado un ítem contacts con permission: 'CONTACT_READ' y hasPermission devolviendo of(false) | Se consulta hasPermission('CONTACT_READ') y el texto sidebarMenu.contacts no aparece | hasPermission.and.returnValue(of(false)) |
should SHOW a menu item with a permission field when the acronym IS granted | Mismo ítem con hasPermission devolviendo of(true) | El texto sidebarMenu.contacts aparece | hasPermission.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 todo | El texto dashboard.titleSidebar aparece y hasPermission nunca se llama | hasPermission y hasAnyPermission devuelven of(false) |
should gate a second-level (children) item on its own permission field, independent of its ungated parent container | Dado un padre sub sin permiso con un hijo con TICKET_READ y otro sin permiso; hasPermission concede solo TICKET_READ | Aparecen el padre, el hijo con permiso y el hijo sin permiso | hasPermission.and.callFake |
Tests en este archivo: 4.
- 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.
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should create the component | Dado el componente creado | Es truthy | Stubs de servicios |
should populate menuItems from NavService.items | Dado el BehaviorSubject inicial de 3 ítems | menuItems.length es 3 | BehaviorSubject |
should update menuItems when NavService.items emits a new value | Cuando items emite un solo ítem z | menuItems tiene 1 elemento con id z | BehaviorSubject |
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should activate a top-level item when its path matches NavigationEnd url | Dado NavigationEnd con /home | El ítem a queda active | Subject de eventos del router |
should activate a second-level item when its path matches NavigationEnd url | Dado NavigationEnd con /players | El padre b queda active | Subject de eventos del router |
should activate a third-level item when its path matches NavigationEnd url | Dado NavigationEnd con /reports/summary | El padre c queda active | Subject de eventos del router |
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should set collapseSidebar to true when it was false | Dado collapseSidebar = false, cuando sidebarToggle() | collapseSidebar es true | navService stub |
should set collapseSidebar to false when it was true | Dado collapseSidebar = true, cuando sidebarToggle() | collapseSidebar es false | navService stub |
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should deactivate all other top-level items when one is activated | Dado dos ítems activos, cuando setNavActive(items[0]) | items[1].active pasa a false | Ninguno |
should activate the parent item when a direct child is the target | Cuando setNavActive(hijo) | El padre queda active | Ninguno |
should activate parent and sub-parent when a grandchild is the target | Cuando setNavActive(nieto) | Padre y subpadre quedan active | Ninguno |
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should set item.active to true when it was false | Dado un ítem inactivo | Queda active true | Ninguno |
should set item.active to false when it was true | Dado un ítem activo | Queda active false | Ninguno |
should deactivate sibling items when activating a top-level item | Dado items[1] activo e items[0] inactivo, cuando toggletNavActive(items[0]) | items[0] activo y items[1] inactivo | Ninguno |
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should return true when at least one child has a path | Dado un hijo con path | Devuelve true | Ninguno |
should return false when no child has a path | Dado un hijo con path indefinido | Devuelve false | Ninguno |
should return false when children is undefined | Dado un ítem sin children | Devuelve false | Ninguno |
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should reset margin to 0 and set leftArrowNone when margin >= -width | Dado margin = -100 y width = 200 | margin 0, leftArrowNone true y rightArrowNone false | Ninguno |
should increment margin by width when margin < -width | Dado margin = -600 y width = 200 | margin -400 y rightArrowNone false | Ninguno |
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should decrement margin by width | Dado margin = 0 y width = 200 | margin -200 y leftArrowNone false | Ninguno |
should cap margin at -3464 and set rightArrowNone when margin <= -3051 | Dado margin = -3051 y width = 200 (borde del tope) | margin -3464, rightArrowNone true y leftArrowNone false | Ninguno |
| it() | Escenario | Resultado esperado | Mocks/spies |
|---|
should update width from the resize event | Dado un evento con innerWidth: 1440 | width es 940 | Evento 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() | Escenario | Resultado esperado | Mocks/spies |
|---|
should hide the content for a userType in the denied list | Dado denied = ['atc'] y usuario atc | No existe .protected | ReplaySubject de perfil |
should show the content for a userType NOT in the denied list | Dado denied = ['atc'] y usuario agent | Existe .protected | ReplaySubject de perfil |
should ALWAYS show the content for admin, even if admin were in the denied list | Dado denied = ['admin'] y usuario admin | Existe .protected (admin siempre ve) | ReplaySubject de perfil |
should show the content when the denied list is empty | Dado denied = [] y usuario atc | Existe .protected | ReplaySubject de perfil |
should fall back to the default hide list when no value is provided | Dado denied = undefined y usuario agent | No existe .protected (lista por defecto) | ReplaySubject de perfil |
should update visibility when the profile userType changes | Dado primero atc y luego admin | Primero oculto, luego visible | ReplaySubject 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() | Escenario | Resultado esperado | Mocks/spies |
|---|
should render the content when the acronym is granted | Dado hasPermission devolviendo of(true) | Existe .protected | hasPermission |
should hide the content when the acronym is NOT granted | Dado hasPermission devolviendo of(false) | No existe .protected | hasPermission |
should default to hidden while the permission check is still pending (deny-by-default) | Dado hasPermission devolviendo un Subject que no emite | No existe .protected (denegado por defecto) | Subject<boolean> pendiente |
should render the else template when denied and provided | Dado hasPermission of(false) y host con else denied | No existe .protected y sí existe .fallback | hasPermission |
should render when ANY acronym in the array is granted | Dado required = ['CONTACT_READ', 'CONTACT_UPDATE'] y hasAnyPermission of(true) | Se llama hasAnyPermission con el array y existe .protected | hasAnyPermission |
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() | Escenario | Resultado esperado | Mocks/spies |
|---|
should return empty string for null | Dado null | Devuelve '' | Ninguno |
should return empty string for undefined | Dado undefined | Devuelve '' | Ninguno |
should return empty string for empty string | Dado '' | Devuelve '' | Ninguno |
should return the original string when shorter than maxLength | Dado Hello con maxLength 10 | Devuelve Hello | Ninguno |
should return the original string when equal to maxLength | Dado Hello con maxLength 5 (borde) | Devuelve Hello | Ninguno |
should truncate and append ellipsis when longer than maxLength | Dado Hello World con maxLength 5 | Devuelve Hello… | Ninguno |
should use default maxLength of 100 | Dado cadenas de 100 y 101 caracteres sin maxLength | La de 100 queda igual y la de 101 se corta a 100 más … | Ninguno |
should truncate correctly with maxLength 0 | Dado abc con maxLength 0 | Devuelve … | 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() | Escenario | Resultado esperado | Mocks/spies |
|---|
should gate CONTACTS on CONTACT_READ via permissionGuard | Dada la ruta PAGE_URLS.CONTACTS | data es { permission: 'CONTACT_READ' } y canActivate contiene permissionGuard | Ninguno |
should gate SETTINGS on SETTINGS_READ via permissionGuard | Dada la ruta PAGE_URLS.SETTINGS | data es { permission: 'SETTINGS_READ' } y canActivate contiene permissionGuard | Ninguno |
should leave DASHBOARD ungated (no data.permission, no permissionGuard) | Dada la ruta PAGE_URLS.DASHBOARD | data es undefined y canActivate no contiene permissionGuard | Ninguno |
should leave PROFILE ungated (self-service, no data.permission, no permissionGuard) | Dada la ruta PAGE_URLS.PROFILE | data es undefined y canActivate no contiene permissionGuard | Ninguno |
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).