From 96adcef3fe95dc894e13fa5aeac60ff6f5d159e3 Mon Sep 17 00:00:00 2001 From: sumeyye Date: Tue, 20 Jan 2026 15:43:11 +0300 Subject: [PATCH] update: theme-shared package tests --- .../breadcrumb-items.component.ts | 2 +- .../lib/tests/breadcrumb.component.spec.ts | 81 ++++++---- .../src/lib/tests/button.component.spec.ts | 2 +- .../src/lib/tests/card.component.spec.ts | 2 +- .../src/lib/tests/checkbox.component.spec.ts | 2 +- .../lib/tests/confirmation.service.spec.ts | 80 ++++------ .../src/lib/tests/ellipsis.directive.spec.ts | 58 ++++++- .../src/lib/tests/error.component.spec.ts | 111 ++++++++----- .../src/lib/tests/error.handler.spec.ts | 43 ++--- .../lib/tests/form-input.component.spec.ts | 2 +- .../lib/tests/loader-bar.component.spec.ts | 149 +++++++++++++----- .../src/lib/tests/loading.directive.spec.ts | 25 +-- .../src/lib/tests/modal.component.spec.ts | 48 +++--- .../theme-shared/src/lib/tests/test-utils.ts | 54 +++++++ .../src/lib/tests/toaster.service.spec.ts | 26 +-- .../src/lib/tests/validation-utils.spec.ts | 2 +- .../packages/theme-shared/src/test-setup.ts | 38 +++-- .../packages/theme-shared/vitest.config.mts | 1 + 18 files changed, 482 insertions(+), 244 deletions(-) create mode 100644 npm/ng-packs/packages/theme-shared/src/lib/tests/test-utils.ts diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts index 6e60a77669..a5a1a4d112 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts @@ -6,7 +6,7 @@ import { ABP, LocalizationPipe } from '@abp/ng.core'; @Component({ selector: 'abp-breadcrumb-items', templateUrl: './breadcrumb-items.component.html', - imports: [ NgTemplateOutlet, RouterLink, LocalizationPipe], + imports: [NgTemplateOutlet, RouterLink, LocalizationPipe], }) export class BreadcrumbItemsComponent { @Input() items: Partial[] = []; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/breadcrumb.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/breadcrumb.component.spec.ts index 5f84edac4f..74a2fcaa36 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/breadcrumb.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/breadcrumb.component.spec.ts @@ -1,31 +1,25 @@ import { ABP, - CORE_OPTIONS, LocalizationPipe, RouterOutletComponent, RoutesService, - LocalizationService, + provideAbpCore, + withOptions, + RestService, + AbpApplicationConfigurationService, + ConfigStateService, } from '@abp/ng.core'; -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { RouterModule } from '@angular/router'; -import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest'; +import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/vitest'; +import { of } from 'rxjs'; import { BreadcrumbComponent, BreadcrumbItemsComponent } from '../components'; -import { OTHERS_GROUP } from '@abp/ng.core'; -import { SORT_COMPARE_FUNC } from '@abp/ng.core'; +import { setupComponentResources } from './test-utils'; const mockRoutes: ABP.Route[] = [ { name: '_::Identity', path: '/identity' }, { name: '_::Users', path: '/identity/users', parentName: '_::Identity' }, ]; -// Simple compare function that doesn't use inject() -const simpleCompareFunc = (a: any, b: any) => { - const aNumber = a.order || 0; - const bNumber = b.order || 0; - return aNumber - bNumber; -}; - describe('BreadcrumbComponent', () => { let spectator: SpectatorRouting; let routes: RoutesService; @@ -34,39 +28,58 @@ describe('BreadcrumbComponent', () => { component: RouterOutletComponent, stubsEnabled: false, detectChanges: false, + imports: [ + RouterModule, + LocalizationPipe, + BreadcrumbComponent, + BreadcrumbItemsComponent, + ], providers: [ - provideHttpClient(), - provideHttpClientTesting(), - { - provide: CORE_OPTIONS, - useValue: { + provideAbpCore( + withOptions({ environment: { apis: { default: { url: 'http://localhost:4200', }, }, + application: { + name: 'TestApp', + baseUrl: 'http://localhost:4200', + }, }, - } + registerLocaleFn: () => Promise.resolve(), + skipGetAppConfiguration: true, + }), + ), + { + provide: RestService, + useValue: { + request: vi.fn(), + handleError: vi.fn(), + }, }, - RoutesService, - LocalizationService, { - provide: OTHERS_GROUP, - useValue: 'AbpUi::OthersGroup', + provide: AbpApplicationConfigurationService, + useValue: { + get: vi.fn(), + }, }, { - provide: SORT_COMPARE_FUNC, - useValue: simpleCompareFunc, + provide: ConfigStateService, + useValue: { + getOne: vi.fn(), + getAll: vi.fn(() => ({})), + getAll$: vi.fn(() => of({})), + getDeep: vi.fn(), + getDeep$: vi.fn(() => of(undefined)), + createOnUpdateStream: vi.fn(() => ({ + subscribe: vi.fn(() => ({ unsubscribe: vi.fn() })) + })), + refreshAppState: vi.fn(), + }, }, ], - declarations: [], - imports: [ - RouterModule, - LocalizationPipe, - BreadcrumbComponent, - BreadcrumbItemsComponent, - ], routes: [ { path: '', @@ -85,6 +98,8 @@ describe('BreadcrumbComponent', () => { ], }); + beforeAll(() => setupComponentResources('../components/breadcrumb', import.meta.url)); + beforeEach(() => { spectator = createRouting(); routes = spectator.inject(RoutesService); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/button.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/button.component.spec.ts index bf834d182d..2986e7c14e 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/button.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/button.component.spec.ts @@ -1,4 +1,4 @@ -import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; import { ButtonComponent } from '../components'; describe('ButtonComponent', () => { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/card.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/card.component.spec.ts index fa474640f6..9990b33676 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/card.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/card.component.spec.ts @@ -1,4 +1,4 @@ -import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; import { CardComponent, CardBodyComponent, diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/checkbox.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/checkbox.component.spec.ts index e2fba6d0da..4ea0946c01 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/checkbox.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/checkbox.component.spec.ts @@ -1,4 +1,4 @@ -import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; import { FormCheckboxComponent } from '../components/checkbox/checkbox.component'; describe('FormCheckboxComponent', () => { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts index 029becaa96..3870ca1185 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts @@ -1,27 +1,24 @@ import { CoreTestingModule } from '@abp/ng.core/testing'; -import { NgModule } from '@angular/core'; -import { fakeAsync, tick } from '@angular/core/testing'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; -import { timer } from 'rxjs'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; +import { firstValueFrom, timer } from 'rxjs'; import { ConfirmationComponent } from '../components'; import { Confirmation } from '../models'; import { ConfirmationService } from '../services'; import { CONFIRMATION_ICONS, DEFAULT_CONFIRMATION_ICONS } from '../tokens/confirmation-icons.token'; - -@NgModule({ - exports: [ConfirmationComponent], - declarations: [], - imports: [CoreTestingModule.withConfig(), ConfirmationComponent], - providers: [{ provide: CONFIRMATION_ICONS, useValue: DEFAULT_CONFIRMATION_ICONS }], -}) -export class MockModule {} +import { setupComponentResources } from './test-utils'; describe('ConfirmationService', () => { let spectator: SpectatorService; let service: ConfirmationService; + const createService = createServiceFactory({ service: ConfirmationService, - imports: [CoreTestingModule.withConfig(), MockModule], + imports: [CoreTestingModule.withConfig(), ConfirmationComponent], + providers: [{ provide: CONFIRMATION_ICONS, useValue: DEFAULT_CONFIRMATION_ICONS }], + }); + + beforeAll(async () => { + await setupComponentResources('../components/confirmation', import.meta.url); }); beforeEach(() => { @@ -33,16 +30,16 @@ describe('ConfirmationService', () => { clearElements(); }); - test('should display a confirmation popup', fakeAsync(() => { + test('should display a confirmation popup', async () => { service.show('_::MESSAGE', '_::TITLE'); - tick(); + await firstValueFrom(timer(10)); expect(selectConfirmationContent('.title')).toBe('TITLE'); expect(selectConfirmationContent('.message')).toBe('MESSAGE'); - })); + }); - test('should display HTML string in title, message, and buttons', fakeAsync(() => { + test('should display HTML string in title, message, and buttons', async () => { service.show( '_::MESSAGE', '_::TITLE', @@ -53,24 +50,24 @@ describe('ConfirmationService', () => { }, ); - tick(); + await firstValueFrom(timer(10)); expect(selectConfirmationContent('.custom-title')).toBe('TITLE'); expect(selectConfirmationContent('.custom-message')).toBe('MESSAGE'); expect(selectConfirmationContent('.custom-cancel')).toBe('CANCEL'); expect(selectConfirmationContent('.custom-yes')).toBe('YES'); - })); + }); - test('should display custom FA icon', fakeAsync(() => { + test('should display custom FA icon', async () => { service.show('_::MESSAGE', '_::TITLE', undefined, { icon: 'fa fa-info', }); - tick(); + await firstValueFrom(timer(10)); expect(selectConfirmationElement('.icon').className).toBe('icon fa fa-info'); - })); + }); - test('should display custom icon as html element', fakeAsync(() => { + test('should display custom icon as html element', async () => { const className = 'custom-icon'; const selector = '.' + className; @@ -78,12 +75,14 @@ describe('ConfirmationService', () => { iconTemplate: `I am icon`, }); - tick(); + await firstValueFrom(timer(10)); const element = selectConfirmationElement(selector); expect(element).toBeTruthy(); expect(element.innerHTML).toBe('I am icon'); - })); + }); + + test.each` type | selector | icon ${'info'} | ${'.info'} | ${'.fa-info-circle'} @@ -93,7 +92,7 @@ describe('ConfirmationService', () => { `('should display $type confirmation popup', async ({ type, selector, icon }) => { service[type]('_::MESSAGE', '_::TITLE'); - await timer(0).toPromise(); + await firstValueFrom(timer(10)); expect(selectConfirmationContent('.title')).toBe('TITLE'); expect(selectConfirmationContent('.message')).toBe('MESSAGE'); @@ -101,31 +100,18 @@ describe('ConfirmationService', () => { expect(selectConfirmationElement(icon)).toBeTruthy(); }); - // test('should close with ESC key', (done) => { - // service - // .info('', '') - // .pipe(take(1)) - // .subscribe((status) => { - // expect(status).toBe(Confirmation.Status.dismiss); - // done(); - // }); - // const escape = new KeyboardEvent('keyup', { key: 'Escape' }); - // document.dispatchEvent(escape); - // }); - - test('should close when click cancel button', done => { + test('should close when click cancel button', async () => { service.info('_::', '_::', { yesText: '_::Sure', cancelText: '_::Exit' }).subscribe(status => { expect(status).toBe(Confirmation.Status.reject); - done(); }); - timer(0).subscribe(() => { - expect(selectConfirmationContent('button#cancel')).toBe('Exit'); - expect(selectConfirmationContent('button#confirm')).toBe('Sure'); + await firstValueFrom(timer(10)); - (document.querySelector('button#cancel') as HTMLButtonElement).click(); - }); + expect(selectConfirmationContent('button#cancel')).toBe('Exit'); + expect(selectConfirmationContent('button#confirm')).toBe('Sure'); + + (document.querySelector('button#cancel') as HTMLButtonElement).click(); }); test.each` @@ -135,9 +121,9 @@ describe('ConfirmationService', () => { `( 'should call the listenToEscape method $count times when dismissible is $dismissible', ({ dismissible, count }) => { - const spy = jest.spyOn(service as any, 'listenToEscape'); + const spy = vi.spyOn(service as any, 'listenToEscape'); - service.info('_::', '_::', { dismissible }); + service.info('_::', '_::', { dismissible }); expect(spy).toHaveBeenCalledTimes(count); }, diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/ellipsis.directive.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/ellipsis.directive.spec.ts index 727a1c5312..e9b0e45a0d 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/ellipsis.directive.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/ellipsis.directive.spec.ts @@ -1,4 +1,4 @@ -import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; +import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/vitest'; import { EllipsisDirective } from '../directives/ellipsis.directive'; describe('EllipsisDirective', () => { @@ -39,17 +39,61 @@ describe('EllipsisDirective', () => { expect(directive.title).toBe('test title'); }); - test('should have element innerText as title if not specified', () => { - spectator.setHostInput({ title: undefined }); + test('should add abp-ellipsis-inline class to element if width is given', () => { + expect(el).toHaveClass('abp-ellipsis-inline'); + }); +}); + +describe('EllipsisDirective when title is not specified', () => { + let spectator: SpectatorDirective; + let directive: EllipsisDirective; + let el: HTMLDivElement; + const createDirective = createDirectiveFactory({ + directive: EllipsisDirective, + }); + + beforeEach(() => { + spectator = createDirective( + '
test content
', + { + hostProps: { + title: undefined, + width: '100px', + }, + }, + ); + directive = spectator.directive; + el = spectator.query('div') as HTMLDivElement; + }); + + test('should have element innerText as title', () => { expect(directive.title).toBe(el.innerText); }); +}); - test('should add abp-ellipsis-inline class to element if width is given', () => { - expect(el).toHaveClass('abp-ellipsis-inline'); +describe('EllipsisDirective when width is not given', () => { + let spectator: SpectatorDirective; + let directive: EllipsisDirective; + let el: HTMLDivElement; + const createDirective = createDirectiveFactory({ + directive: EllipsisDirective, + }); + + beforeEach(() => { + spectator = createDirective( + '
test content
', + { + hostProps: { + title: 'test title', + width: undefined, + }, + }, + ); + directive = spectator.directive; + el = spectator.query('div') as HTMLDivElement; }); - test('should add abp-ellipsis class to element if width is not given', () => { - spectator.setHostInput({ width: undefined }); + test('should add abp-ellipsis class to element', () => { expect(el).toHaveClass('abp-ellipsis'); }); }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.component.spec.ts index 93b8d32f0f..f2a8716082 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.component.spec.ts @@ -1,49 +1,84 @@ -import { CORE_OPTIONS, LocalizationPipe } from '@abp/ng.core'; -import { HttpClient, HttpClientModule } from '@angular/common/http'; -import { ElementRef, Renderer2 } from '@angular/core'; -import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { DOCUMENT } from '@angular/common'; +import { Router } from '@angular/router'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; +import { Pipe, PipeTransform } from '@angular/core'; import { Subject } from 'rxjs'; +import { vi } from 'vitest'; + import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component'; +import { setupComponentResources } from './test-utils'; + +/** + * Mock pipe to avoid ABP DI chain + */ +@Pipe({ name: 'abpLocalization'}) +class MockLocalizationPipe implements PipeTransform { + transform(value: any): any { + return value; + } +} + +describe('HttpErrorWrapperComponent', () => { + let spectator: Spectator; + let createComponent: ReturnType>; -describe('ErrorComponent', () => { - let spectator: SpectatorHost; - const createHost = createHostFactory({ - component: HttpErrorWrapperComponent, - declarations: [], - mocks: [HttpClient], - providers: [ - { provide: CORE_OPTIONS, useValue: {} }, - { provide: Renderer2, useValue: { removeChild: () => null } }, - { - provide: ElementRef, - useValue: { nativeElement: document.createElement('div') }, - }, - ], - imports: [HttpClientModule, LocalizationPipe], + beforeAll(async () => { + await setupComponentResources( + '../components/http-error-wrapper', + import.meta.url, + ); }); - beforeEach(() => { - spectator = createHost( - '', - ); - spectator.component.destroy$ = new Subject(); - }); - - describe('#destroy', () => { - it('should be call when pressed the esc key', done => { - spectator.component.destroy$.subscribe(() => { - done(); - }); + beforeEach(() => { + if (!createComponent) { + createComponent = createComponentFactory({ + component: HttpErrorWrapperComponent, + detectChanges: false, - spectator.keyboard.pressEscape(); - }); + overrideComponents: [ + [ + HttpErrorWrapperComponent, + { + set: { + template: '
', + imports: [MockLocalizationPipe], + }, + }, + ], + ], - it('should be call when clicked the close button', done => { - spectator.component.destroy$.subscribe(() => { - done(); + providers: [ + { + provide: DOCUMENT, + useValue: document, + }, + { + provide: Router, + useValue: { + navigateByUrl: vi.fn(), + }, + }, + ], }); + } + + spectator = createComponent(); + + spectator.component.destroy$ = new Subject(); + spectator.component.title = '_::Oops!'; + spectator.component.details = '_::Sorry, an error has occured.'; + }); + + it('should create component', () => { + expect(spectator.component).toBeTruthy(); + }); + + it('should emit destroy$ when destroy is called', () => { + const spy = vi.fn(); + spectator.component.destroy$.subscribe(spy); + + spectator.component.destroy(); - spectator.click('#abp-close-button'); - }); + expect(spy).toHaveBeenCalled(); }); }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts index 75726103d2..d57f75af28 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts @@ -2,46 +2,48 @@ import { HttpErrorReporterService } from '@abp/ng.core'; import { CoreTestingModule } from '@abp/ng.core/testing'; import { APP_BASE_HREF } from '@angular/common'; import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; -import { NgModule } from '@angular/core'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; import { OAuthService } from 'angular-oauth2-oidc'; import { of, Subject } from 'rxjs'; -import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component'; import { ErrorHandler } from '../handlers'; import { ConfirmationService } from '../services'; +import { CreateErrorComponentService } from '../services/create-error-component.service'; +import { RouterErrorHandlerService } from '../services/router-error-handler.service'; import { CUSTOM_ERROR_HANDLERS, HTTP_ERROR_CONFIG } from '../tokens/http-error.token'; import { CustomHttpErrorHandlerService } from '../models'; const customHandlerMock: CustomHttpErrorHandlerService = { priority: 100, - canHandle: jest.fn().mockReturnValue(true), - execute: jest.fn(), + canHandle: vi.fn().mockReturnValue(true), + execute: vi.fn(), }; const reporter$ = new Subject(); -@NgModule({ - exports: [HttpErrorWrapperComponent], - declarations: [], - imports: [CoreTestingModule, HttpErrorWrapperComponent], -}) -class MockModule {} - let spectator: SpectatorService; let service: ErrorHandler; let httpErrorReporter: HttpErrorReporterService; -const errorConfirmation: jest.Mock = jest.fn(() => of(null)); -const CONFIRMATION_BUTTONS = { - hideCancelBtn: true, - yesText: 'AbpAccount::Close', -}; +const errorConfirmation = vi.fn(() => of(null)); + describe('ErrorHandler', () => { const createService = createServiceFactory({ service: ErrorHandler, - imports: [CoreTestingModule.withConfig(), MockModule], + imports: [CoreTestingModule.withConfig()], mocks: [OAuthService], providers: [ + { + provide: RouterErrorHandlerService, + useValue: { + listen: vi.fn(), + }, + }, + { + provide: CreateErrorComponentService, + useValue: { + execute: vi.fn(), + }, + }, { provide: HttpErrorReporterService, useValue: { @@ -65,7 +67,10 @@ describe('ErrorHandler', () => { }, { provide: HTTP_ERROR_CONFIG, - useFactory: () => ({}), + useValue: { + skipHandledErrorCodes: [], + errorScreen: {}, + }, }, ], }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/form-input.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/form-input.component.spec.ts index cf4eb4aac3..e6ae275a95 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/form-input.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/form-input.component.spec.ts @@ -1,4 +1,4 @@ -import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; import { FormInputComponent } from '../components/form-input/form-input.component'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/loader-bar.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/loader-bar.component.spec.ts index e2904e33f1..5d6a90173f 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/loader-bar.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/loader-bar.component.spec.ts @@ -1,26 +1,32 @@ -import { HttpWaitService, LOADER_DELAY, SubscriptionService } from '@abp/ng.core'; +import { HttpWaitService, LOADER_DELAY, RouterWaitService, SubscriptionService } from '@abp/ng.core'; import { HttpRequest } from '@angular/common/http'; -import { NavigationEnd, NavigationError, NavigationStart, Router } from '@angular/router'; -import { createComponentFactory, Spectator, SpyObject } from '@ngneat/spectator/jest'; -import { Subject, timer } from 'rxjs'; +import { NavigationStart, Router } from '@angular/router'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; +import { combineLatest, firstValueFrom, Subject, timer } from 'rxjs'; import { LoaderBarComponent } from '../components/loader-bar/loader-bar.component'; +import { setupComponentResources } from './test-utils'; describe('LoaderBarComponent', () => { let spectator: Spectator; let router: Router; + let createComponent: ReturnType>; const events$ = new Subject(); - const createComponent = createComponentFactory({ - component: LoaderBarComponent, - detectChanges: false, - providers: [ - SubscriptionService, - { provide: Router, useValue: { events: events$ } }, - { provide: LOADER_DELAY, useValue: 0 }, - ], - }); + beforeAll(() => setupComponentResources('../components/loader-bar', import.meta.url)); beforeEach(() => { + if (!createComponent) { + createComponent = createComponentFactory({ + component: LoaderBarComponent, + detectChanges: false, + providers: [ + SubscriptionService, + { provide: Router, useValue: { events: events$ } }, + { provide: LOADER_DELAY, useValue: 0 }, + ], + }); + } + spectator = createComponent({}); spectator.component.intervalPeriod = 1; spectator.component.stopDelay = 1; @@ -32,66 +38,127 @@ describe('LoaderBarComponent', () => { expect(spectator.component.color).toBe('#77b6ff'); }); - it('should increase the progressLevel', done => { + it('should increase the progressLevel', async () => { spectator.detectChanges(); const httpWaitService = spectator.inject(HttpWaitService); httpWaitService.addRequest(new HttpRequest('GET', 'test')); spectator.detectChanges(); - setTimeout(() => { - expect(spectator.component.progressLevel > 0).toBeTruthy(); - done(); - }, 10); + + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(spectator.component.progressLevel > 0).toBeTruthy(); }); - it('should be interval unsubscribed', done => { - const request = new HttpRequest('GET', 'test'); + it('should be interval unsubscribed', async () => { + const request = new HttpRequest('GET', 'test'); spectator.detectChanges(); const httpWaitService = spectator.inject(HttpWaitService); + + await firstValueFrom(combineLatest([ + httpWaitService.getLoading$(), + spectator.inject(RouterWaitService).getLoading$() + ])); + httpWaitService.addRequest(request); + spectator.detectChanges(); + + let attempts = 0; + while (spectator.component.interval.closed && attempts < 50) { + await new Promise(resolve => setTimeout(resolve, 10)); + spectator.detectChanges(); + attempts++; + } + expect(spectator.component.interval.closed).toBe(false); + httpWaitService.deleteRequest(request); - timer(400).subscribe(() => { - expect(spectator.component.interval.closed).toBe(true); - done(); - }); + spectator.detectChanges(); + + await firstValueFrom(timer(400)); + + expect(spectator.component.interval.closed).toBe(true); }); - it('should start and stop the loading with navigation', done => { + + it('should start and stop the loading with navigation', async () => { + spectator.detectChanges(); + const routerWaitService = spectator.inject(RouterWaitService); + + routerWaitService.setLoading(true); spectator.detectChanges(); - events$.next(new NavigationStart(1, 'test')); + + let attempts = 0; + while (spectator.component.interval.closed && attempts < 50) { + await new Promise(resolve => setTimeout(resolve, 10)); + spectator.detectChanges(); + attempts++; + } expect(spectator.component.interval.closed).toBe(false); - events$.next(new NavigationEnd(1, 'test', 'test')); - events$.next(new NavigationError(1, 'test', 'test')); + routerWaitService.setLoading(false); + spectator.detectChanges(); + + attempts = 0; + while (spectator.component.progressLevel !== 100 && attempts < 50) { + await new Promise(resolve => setTimeout(resolve, 10)); + spectator.detectChanges(); + attempts++; + } expect(spectator.component.progressLevel).toBe(100); - timer(2).subscribe(() => { - expect(spectator.component.progressLevel).toBe(0); - done(); - }); + await firstValueFrom(timer(spectator.component.stopDelay + 10)); + expect(spectator.component.progressLevel).toBe(0); }); - it('should stop the loading with navigation', done => { + it('should stop the loading with navigation', async () => { + spectator.detectChanges(); + const routerWaitService = spectator.inject(RouterWaitService); + + routerWaitService.setLoading(true); spectator.detectChanges(); - events$.next(new NavigationStart(1, 'test')); + + let attempts = 0; + while (spectator.component.interval.closed && attempts < 50) { + await new Promise(resolve => setTimeout(resolve, 10)); + spectator.detectChanges(); + attempts++; + } expect(spectator.component.interval.closed).toBe(false); - events$.next(new NavigationEnd(1, 'testend', 'testend')); + routerWaitService.setLoading(false); + spectator.detectChanges(); + + attempts = 0; + while (spectator.component.progressLevel !== 100 && attempts < 50) { + await new Promise(resolve => setTimeout(resolve, 10)); + spectator.detectChanges(); + attempts++; + } expect(spectator.component.progressLevel).toBe(100); - timer(2).subscribe(() => { - expect(spectator.component.progressLevel).toBe(0); - done(); - }); + await firstValueFrom(timer(spectator.component.stopDelay + 10)); + expect(spectator.component.progressLevel).toBe(0); }); describe('#startLoading', () => { - it('should return when isLoading is true', done => { + it('should return when isLoading is true', async () => { spectator.detectChanges(); + events$.next(new NavigationStart(1, 'test')); + spectator.detectChanges(); + + let attempts = 0; + while (spectator.component.interval.closed && attempts < 50) { + await new Promise(resolve => setTimeout(resolve, 10)); + spectator.detectChanges(); + attempts++; + } + events$.next(new NavigationStart(1, 'test')); - done(); + spectator.detectChanges(); + + expect(spectator.component).toBeTruthy(); }); }); }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts index 91589f2155..f80388d478 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts @@ -1,4 +1,4 @@ -import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator/jest'; +import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator/vitest'; import { LoadingDirective } from '../directives'; import { LoadingComponent } from '../components'; import { Component } from '@angular/core'; @@ -29,10 +29,11 @@ describe('LoadingDirective', () => { expect(spectator.directive).toBeTruthy(); }); - it('should handle loading input', () => { - spectator.setHostInput({ loading: false }); - spectator.detectChanges(); + it('should handle loading input', async () => { + spectator.directive.loading = false; + await new Promise(resolve => setTimeout(resolve, 10)); expect(spectator.directive).toBeTruthy(); + expect(spectator.directive.loading).toBe(false); }); }); @@ -53,19 +54,19 @@ describe('LoadingDirective', () => { expect(spectator.directive.targetElement).toBe(mockTarget); }); - it('should handle delay input', () => { - spectator.setHostInput({ delay: 100 }); - spectator.detectChanges(); + it('should handle delay input', async () => { + spectator.directive.delay = 100; + await new Promise(resolve => setTimeout(resolve, 10)); expect(spectator.directive).toBeTruthy(); }); - it('should handle loading state changes', () => { - spectator.setHostInput({ loading: false }); - spectator.detectChanges(); + it('should handle loading state changes', async() => { + spectator.directive.loading = false; + await new Promise(resolve => setTimeout(resolve, 10)); expect(spectator.directive).toBeTruthy(); - spectator.setHostInput({ loading: true }); - spectator.detectChanges(); + spectator.directive.loading = true; + await new Promise(resolve => setTimeout(resolve, 10)); expect(spectator.directive).toBeTruthy(); }); }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts index dc79c47a9c..28982aefe2 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts @@ -1,10 +1,11 @@ import { ConfirmationService } from '@abp/ng.theme.shared'; import { CoreTestingModule } from '@abp/ng.core/testing'; import { Component, EventEmitter, Input } from '@angular/core'; -import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; import { Confirmation } from '@abp/ng.theme.shared'; -import { Subject, timer } from 'rxjs'; +import { firstValueFrom, Subject, timer } from 'rxjs'; import { ModalComponent } from '../components/modal/modal.component'; +import { setupComponentResources } from './test-utils'; @Component({ template: ` @@ -27,25 +28,34 @@ class TestHostComponent { } const mockConfirmation$ = new Subject(); -const disappearFn = jest.fn(); +const disappearFn = vi.fn(); describe('ModalComponent', () => { let spectator: Spectator; + let createComponent: ReturnType>; - const createComponent = createComponentFactory({ - component: TestHostComponent, - imports: [CoreTestingModule.withConfig()], - providers: [ - { - provide: ConfirmationService, - useValue: { - warn: jest.fn(() => mockConfirmation$), - }, - }, - ], - }); + beforeAll(() => setupComponentResources('../components/modal', import.meta.url)); beforeEach(() => { + // Create component factory in beforeEach to ensure beforeAll has run + if (!createComponent) { + createComponent = createComponentFactory({ + component: TestHostComponent, + imports: [ + CoreTestingModule.withConfig(), + ModalComponent, + ], + providers: [ + { + provide: ConfirmationService, + useValue: { + warn: vi.fn(() => mockConfirmation$), + }, + }, + ], + }); + } + spectator = createComponent(); disappearFn.mockClear(); }); @@ -71,10 +81,10 @@ describe('ModalComponent', () => { }); }); -async function wait0ms() { - await timer(0).toPromise(); +async function wait0ms() { + await firstValueFrom(timer(0)); } -async function wait300ms() { - await timer(300).toPromise(); +async function wait300ms() { + await firstValueFrom(timer(300)); } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/test-utils.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/test-utils.ts new file mode 100644 index 0000000000..ac9c9eff50 --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/test-utils.ts @@ -0,0 +1,54 @@ +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** + * Sets up component resource resolution for Angular component tests. + * This is needed when components have external templates or stylesheets. + * + * @param componentDirPath - The path to the component directory relative to the test file. + * For example: '../components/loader-bar' or './components/my-component' + * @param testFileUrl - The import.meta.url from the test file. Defaults to the caller's location. + * + * @example + * ```typescript + * + * import { setupComponentResources } from './test-utils'; + * + * beforeAll(() => setupComponentResources('../components/loader-bar', import.meta.url)); + * ``` + */ +export async function setupComponentResources( + componentDirPath: string, + testFileUrl: string = import.meta.url, +): Promise { + try { + if (typeof process !== 'undefined' && process.versions?.node) { + const { ɵresolveComponentResources: resolveComponentResources } = await import('@angular/core'); + + // Get the test file directory path + const testFileDir = dirname(fileURLToPath(testFileUrl)); + const componentDir = resolve(testFileDir, componentDirPath); + + await resolveComponentResources((url: string) => { + // For SCSS/SASS files, return empty CSS since jsdom can't parse SCSS + if (url.endsWith('.scss') || url.endsWith('.sass')) { + return Promise.resolve(''); + } + + // For other files (HTML, CSS, etc.), read the actual content + try { + // Resolve relative paths like './component.scss' or 'component.scss' + const normalizedUrl = url.replace(/^\.\//, ''); + const filePath = resolve(componentDir, normalizedUrl); + return Promise.resolve(readFileSync(filePath, 'utf-8')); + } catch (error) { + // If file not found, return empty string + return Promise.resolve(''); + } + }); + } + } catch (error) { + console.warn('Failed to set up component resource resolver:', error); + } +} diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts index ae008d73ee..401da50070 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts @@ -1,23 +1,25 @@ -import { CoreTestingModule } from '@abp/ng.core/testing'; -import { NgModule } from '@angular/core'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { ContentProjectionService } from '@abp/ng.core'; +import { ComponentRef } from '@angular/core'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import { ToastContainerComponent } from '../components/toast-container/toast-container.component'; -import { ToastComponent } from '../components/toast/toast.component'; import { ToasterService } from '../services/toaster.service'; -@NgModule({ - exports: [ToastContainerComponent], - declarations: [], - imports: [CoreTestingModule.withConfig(), ToastContainerComponent, ToastComponent], -}) -export class MockModule {} - describe('ToasterService', () => { let spectator: SpectatorService; let service: ToasterService; + const mockComponentRef = { + changeDetectorRef: { detectChanges: vi.fn() }, + instance: {} as ToastContainerComponent, + } as unknown as ComponentRef; + + const contentProjectionService = { + projectContent: vi.fn().mockReturnValue(mockComponentRef), + } satisfies Partial; + const createService = createServiceFactory({ service: ToasterService, - imports: [CoreTestingModule.withConfig(), MockModule], + providers: [{ provide: ContentProjectionService, useValue: contentProjectionService }], }); beforeEach(() => { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/validation-utils.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/validation-utils.spec.ts index 9f263779e2..991a14f120 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/validation-utils.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/validation-utils.spec.ts @@ -2,7 +2,7 @@ import { AbpApplicationConfigurationService, ConfigStateService } from '@abp/ng. import { CoreTestingModule } from '@abp/ng.core/testing'; import { HttpClient } from '@angular/common/http'; import { Component, Injector } from '@angular/core'; -import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; import { OAuthService } from 'angular-oauth2-oidc'; import { of } from 'rxjs'; import { getPasswordValidators, validatePassword } from '../utils'; diff --git a/npm/ng-packs/packages/theme-shared/src/test-setup.ts b/npm/ng-packs/packages/theme-shared/src/test-setup.ts index 2cf1ac7191..3eaa53b84d 100644 --- a/npm/ng-packs/packages/theme-shared/src/test-setup.ts +++ b/npm/ng-packs/packages/theme-shared/src/test-setup.ts @@ -1,10 +1,28 @@ -import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; -setupZoneTestEnv(); - -const originalError = console.error; -console.error = (...args: any[]) => { - if (args[0]?.includes?.('ExpressionChangedAfterItHasBeenCheckedError')) { - return; - } - originalError.apply(console, args); -}; +import '@angular/compiler'; +import 'zone.js'; +import 'zone.js/testing'; +import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; +import { + ɵgetCleanupHook as getCleanupHook, + getTestBed +} from '@angular/core/testing'; + + +beforeEach(getCleanupHook(false)); +afterEach(getCleanupHook(true)); + +// Initialize Angular testing environment +getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); + + +// Mock window.location for test environment +Object.defineProperty(window, 'location', { + value: { + href: 'http://localhost:4200', + origin: 'http://localhost:4200', + pathname: '/', + search: '', + hash: '', + }, + writable: true, +}); diff --git a/npm/ng-packs/packages/theme-shared/vitest.config.mts b/npm/ng-packs/packages/theme-shared/vitest.config.mts index 0b64f9ceba..d9496b26fd 100644 --- a/npm/ng-packs/packages/theme-shared/vitest.config.mts +++ b/npm/ng-packs/packages/theme-shared/vitest.config.mts @@ -11,6 +11,7 @@ export default defineConfig(() => ({ watch: false, globals: true, environment: 'jsdom', + setupFiles: ['src/test-setup.ts'], include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], reporters: ['default'], coverage: {