mirror of https://github.com/abpframework/abp.git
Browse Source
Adds and refines unit tests for core Angular services and utilities, including LocalizationService, DynamicLayoutComponent, generator and date utilities, lazy loading, and initialization logic. Tests now cover more edge cases, error handling, and additional scenarios for context, fallback, and configuration-driven behaviors. Also includes minor fixes to localization resource merging logic.pull/23460/head
20 changed files with 1517 additions and 735 deletions
@ -1,201 +1,281 @@ |
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { Component, NgModule, inject as inject_1 } from '@angular/core'; |
|||
import { ActivatedRoute, RouterModule } from '@angular/router'; |
|||
import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest'; |
|||
import { DynamicLayoutComponent, RouterOutletComponent } from '../components'; |
|||
import { eLayoutType } from '../enums/common'; |
|||
import { ABP } from '../models'; |
|||
import { AbpApplicationConfigurationService } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service'; |
|||
import { ReplaceableComponentsService, RoutesService } from '../services'; |
|||
import { mockRoutesService } from './routes.service.spec'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-layout-application', |
|||
template: '<router-outlet></router-outlet>', |
|||
}) |
|||
class DummyApplicationLayoutComponent {} |
|||
|
|||
@Component({ |
|||
selector: 'abp-layout-account', |
|||
template: '<router-outlet></router-outlet>', |
|||
}) |
|||
class DummyAccountLayoutComponent {} |
|||
|
|||
@Component({ |
|||
selector: 'abp-layout-empty', |
|||
template: '<router-outlet></router-outlet>', |
|||
}) |
|||
class DummyEmptyLayoutComponent {} |
|||
|
|||
const LAYOUTS = [ |
|||
DummyApplicationLayoutComponent, |
|||
DummyAccountLayoutComponent, |
|||
DummyEmptyLayoutComponent, |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule], |
|||
declarations: [...LAYOUTS], |
|||
}) |
|||
class DummyLayoutModule {} |
|||
|
|||
@Component({ |
|||
selector: 'abp-dummy', |
|||
template: '{{route.snapshot.data?.name}} works!', |
|||
}) |
|||
class DummyComponent {
route = inject_1(ActivatedRoute); |
|||
|
|||
} |
|||
|
|||
const routes: ABP.Route[] = [ |
|||
{ |
|||
path: '', |
|||
name: 'Root', |
|||
}, |
|||
{ |
|||
path: '/parentWithLayout', |
|||
name: 'ParentWithLayout', |
|||
parentName: 'Root', |
|||
layout: eLayoutType.application, |
|||
}, |
|||
{ |
|||
path: '/parentWithLayout/childWithoutLayout', |
|||
name: 'ChildWithoutLayout', |
|||
parentName: 'ParentWithLayout', |
|||
}, |
|||
{ |
|||
path: '/parentWithLayout/childWithLayout', |
|||
name: 'ChildWithLayout', |
|||
parentName: 'ParentWithLayout', |
|||
layout: eLayoutType.account, |
|||
}, |
|||
{ |
|||
path: '/withData', |
|||
name: 'WithData', |
|||
layout: eLayoutType.application, |
|||
}, |
|||
]; |
|||
|
|||
describe('DynamicLayoutComponent', () => { |
|||
const createComponent = createRoutingFactory({ |
|||
component: RouterOutletComponent, |
|||
stubsEnabled: false, |
|||
declarations: [DummyComponent, DynamicLayoutComponent], |
|||
mocks: [AbpApplicationConfigurationService, HttpClient], |
|||
providers: [ |
|||
{ |
|||
provide: RoutesService, |
|||
useFactory: () => mockRoutesService(), |
|||
}, |
|||
ReplaceableComponentsService, |
|||
], |
|||
imports: [RouterModule, DummyLayoutModule], |
|||
routes: [ |
|||
{ path: '', component: RouterOutletComponent }, |
|||
{ |
|||
path: 'parentWithLayout', |
|||
component: DynamicLayoutComponent, |
|||
children: [ |
|||
{ |
|||
path: 'childWithoutLayout', |
|||
component: DummyComponent, |
|||
data: { name: 'childWithoutLayout' }, |
|||
}, |
|||
{ |
|||
path: 'childWithLayout', |
|||
component: DummyComponent, |
|||
data: { name: 'childWithLayout' }, |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
path: 'withData', |
|||
component: DynamicLayoutComponent, |
|||
children: [ |
|||
{ |
|||
path: '', |
|||
component: DummyComponent, |
|||
data: { name: 'withData' }, |
|||
}, |
|||
], |
|||
data: { layout: eLayoutType.empty }, |
|||
}, |
|||
{ |
|||
path: 'withoutLayout', |
|||
component: DynamicLayoutComponent, |
|||
children: [ |
|||
{ |
|||
path: '', |
|||
component: DummyComponent, |
|||
data: { name: 'withoutLayout' }, |
|||
}, |
|||
], |
|||
data: { layout: null }, |
|||
}, |
|||
], |
|||
}); |
|||
|
|||
let spectator: SpectatorRouting<RouterOutletComponent>; |
|||
let replaceableComponents: ReplaceableComponentsService; |
|||
|
|||
beforeEach(async () => { |
|||
spectator = createComponent(); |
|||
replaceableComponents = spectator.inject(ReplaceableComponentsService); |
|||
const routesService = spectator.inject(RoutesService); |
|||
routesService.add(routes); |
|||
|
|||
replaceableComponents.add({ |
|||
key: 'Theme.ApplicationLayoutComponent', |
|||
component: DummyApplicationLayoutComponent, |
|||
}); |
|||
replaceableComponents.add({ |
|||
key: 'Theme.AccountLayoutComponent', |
|||
component: DummyAccountLayoutComponent, |
|||
}); |
|||
replaceableComponents.add({ |
|||
key: 'Theme.EmptyLayoutComponent', |
|||
component: DummyEmptyLayoutComponent, |
|||
}); |
|||
}); |
|||
|
|||
it('should handle application layout from parent abp route and display it', async () => { |
|||
spectator.router.navigateByUrl('/parentWithLayout/childWithoutLayout'); |
|||
await spectator.fixture.whenStable(); |
|||
spectator.detectComponentChanges(); |
|||
expect(spectator.query('abp-dynamic-layout')).toBeTruthy(); |
|||
expect(spectator.query('abp-layout-application')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should handle account layout from own property and display it', async () => { |
|||
spectator.router.navigateByUrl('/parentWithLayout/childWithLayout'); |
|||
await spectator.fixture.whenStable(); |
|||
spectator.detectComponentChanges(); |
|||
expect(spectator.query('abp-layout-account')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should handle empty layout from route data and display it', async () => { |
|||
spectator.router.navigateByUrl('/withData'); |
|||
await spectator.fixture.whenStable(); |
|||
spectator.detectComponentChanges(); |
|||
expect(spectator.query('abp-layout-empty')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should display empty layout when layout is null', async () => { |
|||
spectator.router.navigateByUrl('/withoutLayout'); |
|||
await spectator.fixture.whenStable(); |
|||
spectator.detectComponentChanges(); |
|||
expect(spectator.query('abp-layout-empty')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should not display any layout when layouts are empty', async () => { |
|||
const spy = jest.spyOn(replaceableComponents, 'get'); |
|||
spy.mockReturnValue(null); |
|||
spectator.detectChanges(); |
|||
|
|||
spectator.router.navigateByUrl('/withoutLayout'); |
|||
await spectator.fixture.whenStable(); |
|||
spectator.detectComponentChanges(); |
|||
|
|||
expect(spectator.query('abp-layout-empty')).toBeFalsy(); |
|||
}); |
|||
}); |
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { Component, inject as inject_1 } from '@angular/core'; |
|||
import { ActivatedRoute, RouterModule, RouterOutlet } from '@angular/router'; |
|||
import { of, BehaviorSubject } from 'rxjs'; |
|||
import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest'; |
|||
import { DynamicLayoutComponent, RouterOutletComponent } from '../components'; |
|||
import { eLayoutType } from '../enums/common'; |
|||
import { ABP } from '../models'; |
|||
import { AbpApplicationConfigurationService } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service'; |
|||
import { ReplaceableComponentsService, RoutesService, RouterEvents, EnvironmentService, LocalizationService } from '../services'; |
|||
import { DYNAMIC_LAYOUTS_TOKEN } from '../tokens/dynamic-layout.token'; |
|||
|
|||
const mockRoutesService = () => ({ |
|||
add: jest.fn(), |
|||
find: jest.fn((predicate) => { |
|||
|
|||
if (predicate && typeof predicate === 'function') { |
|||
if (predicate({ path: '/parentWithLayout/childWithoutLayout' })) { |
|||
return { layout: eLayoutType.application }; |
|||
} |
|||
if (predicate({ path: '/parentWithLayout/childWithLayout' })) { |
|||
return { layout: eLayoutType.account }; |
|||
} |
|||
if (predicate({ path: '/withData' })) { |
|||
return { layout: eLayoutType.empty }; |
|||
} |
|||
if (predicate({ path: '/withoutLayout' })) { |
|||
return { layout: null }; |
|||
} |
|||
} |
|||
return null; |
|||
}), |
|||
search: jest.fn((query) => { |
|||
|
|||
if (query && query.invisible) { |
|||
return { layout: eLayoutType.account }; |
|||
} |
|||
return null; |
|||
}), |
|||
flat$: of([]), |
|||
tree$: of([]), |
|||
visible$: of([]), |
|||
}); |
|||
|
|||
@Component({ |
|||
selector: 'abp-layout-application', |
|||
template: '<router-outlet></router-outlet>', |
|||
standalone: true, |
|||
imports: [RouterOutlet], |
|||
}) |
|||
class DummyApplicationLayoutComponent {} |
|||
|
|||
@Component({ |
|||
selector: 'abp-layout-account', |
|||
template: '<router-outlet></router-outlet>', |
|||
standalone: true, |
|||
imports: [RouterOutlet], |
|||
}) |
|||
class DummyAccountLayoutComponent {} |
|||
|
|||
@Component({ |
|||
selector: 'abp-layout-empty', |
|||
template: '<router-outlet></router-outlet>', |
|||
standalone: true, |
|||
imports: [RouterOutlet], |
|||
}) |
|||
class DummyEmptyLayoutComponent {} |
|||
|
|||
const LAYOUTS = [ |
|||
DummyApplicationLayoutComponent, |
|||
DummyAccountLayoutComponent, |
|||
DummyEmptyLayoutComponent, |
|||
]; |
|||
|
|||
@Component({ |
|||
selector: 'abp-dummy', |
|||
template: '{{route.snapshot.data?.name}} works!', |
|||
standalone: true, |
|||
}) |
|||
class DummyComponent { |
|||
route = inject_1(ActivatedRoute); |
|||
} |
|||
|
|||
const routes: ABP.Route[] = [ |
|||
{ |
|||
path: '', |
|||
name: 'Root', |
|||
}, |
|||
{ |
|||
path: '/parentWithLayout', |
|||
name: 'ParentWithLayout', |
|||
parentName: 'Root', |
|||
layout: eLayoutType.application, |
|||
}, |
|||
{ |
|||
path: '/parentWithLayout/childWithoutLayout', |
|||
name: 'ChildWithoutLayout', |
|||
parentName: 'ParentWithLayout', |
|||
}, |
|||
{ |
|||
path: '/parentWithLayout/childWithLayout', |
|||
name: 'ChildWithLayout', |
|||
parentName: 'ParentWithLayout', |
|||
layout: eLayoutType.account, |
|||
}, |
|||
{ |
|||
path: '/withData', |
|||
name: 'WithData', |
|||
layout: eLayoutType.application, |
|||
}, |
|||
]; |
|||
|
|||
describe('DynamicLayoutComponent', () => { |
|||
const createComponent = createRoutingFactory({ |
|||
component: DynamicLayoutComponent, |
|||
stubsEnabled: false, |
|||
declarations: [], |
|||
mocks: [AbpApplicationConfigurationService, HttpClient], |
|||
providers: [ |
|||
{ |
|||
provide: RoutesService, |
|||
useValue: mockRoutesService(), |
|||
}, |
|||
{ |
|||
provide: RouterEvents, |
|||
useValue: { |
|||
getNavigationEvents: jest.fn().mockReturnValue(of({})), |
|||
}, |
|||
}, |
|||
{ |
|||
provide: EnvironmentService, |
|||
useValue: { |
|||
getEnvironment: jest.fn().mockReturnValue({ |
|||
oAuthConfig: { responseType: 'code' }, |
|||
}), |
|||
}, |
|||
}, |
|||
{ |
|||
provide: LocalizationService, |
|||
useValue: { |
|||
languageChange$: new BehaviorSubject('en'), |
|||
}, |
|||
}, |
|||
{ |
|||
provide: DYNAMIC_LAYOUTS_TOKEN, |
|||
useValue: new Map([ |
|||
[eLayoutType.application, 'Theme.ApplicationLayoutComponent'], |
|||
[eLayoutType.account, 'Theme.AccountLayoutComponent'], |
|||
[eLayoutType.empty, 'Theme.EmptyLayoutComponent'], |
|||
]), |
|||
}, |
|||
{ |
|||
provide: ReplaceableComponentsService, |
|||
useValue: { |
|||
add: jest.fn(), |
|||
get: jest.fn((key) => { |
|||
if (key === 'Theme.ApplicationLayoutComponent') { |
|||
return { component: DummyApplicationLayoutComponent }; |
|||
} |
|||
if (key === 'Theme.AccountLayoutComponent') { |
|||
return { component: DummyAccountLayoutComponent }; |
|||
} |
|||
if (key === 'Theme.EmptyLayoutComponent') { |
|||
return { component: DummyEmptyLayoutComponent }; |
|||
} |
|||
return null; |
|||
}), |
|||
}, |
|||
}, |
|||
], |
|||
imports: [RouterModule, DummyComponent, DynamicLayoutComponent, ...LAYOUTS], |
|||
routes: [ |
|||
{ path: '', component: RouterOutletComponent }, |
|||
{ |
|||
path: 'parentWithLayout', |
|||
component: DynamicLayoutComponent, |
|||
children: [ |
|||
{ |
|||
path: 'childWithoutLayout', |
|||
component: DummyComponent, |
|||
data: { name: 'childWithoutLayout' }, |
|||
}, |
|||
{ |
|||
path: 'childWithLayout', |
|||
component: DummyComponent, |
|||
data: { name: 'childWithLayout' }, |
|||
}, |
|||
], |
|||
}, |
|||
{ |
|||
path: 'withData', |
|||
component: DynamicLayoutComponent, |
|||
children: [ |
|||
{ |
|||
path: '', |
|||
component: DummyComponent, |
|||
data: { name: 'withData' }, |
|||
}, |
|||
], |
|||
data: { layout: eLayoutType.empty }, |
|||
}, |
|||
{ |
|||
path: 'withoutLayout', |
|||
component: DynamicLayoutComponent, |
|||
children: [ |
|||
{ |
|||
path: '', |
|||
component: DummyComponent, |
|||
data: { name: 'withoutLayout' }, |
|||
}, |
|||
], |
|||
data: { layout: null }, |
|||
}, |
|||
], |
|||
}); |
|||
|
|||
let spectator: SpectatorRouting<DynamicLayoutComponent>; |
|||
let replaceableComponents: ReplaceableComponentsService; |
|||
|
|||
beforeEach(async () => { |
|||
spectator = createComponent(); |
|||
replaceableComponents = spectator.inject(ReplaceableComponentsService); |
|||
const routesService = spectator.inject(RoutesService); |
|||
routesService.add(routes); |
|||
|
|||
replaceableComponents.add({ |
|||
key: 'Theme.ApplicationLayoutComponent', |
|||
component: DummyApplicationLayoutComponent, |
|||
}); |
|||
replaceableComponents.add({ |
|||
key: 'Theme.AccountLayoutComponent', |
|||
component: DummyAccountLayoutComponent, |
|||
}); |
|||
replaceableComponents.add({ |
|||
key: 'Theme.EmptyLayoutComponent', |
|||
component: DummyEmptyLayoutComponent, |
|||
}); |
|||
}); |
|||
|
|||
it('should handle application layout from parent abp route and display it', async () => { |
|||
spectator.router.navigateByUrl('/parentWithLayout/childWithoutLayout'); |
|||
await spectator.fixture.whenStable(); |
|||
await new Promise(resolve => setTimeout(resolve, 100)); |
|||
spectator.detectComponentChanges(); |
|||
expect(spectator.query('abp-dynamic-layout')).toBeTruthy(); |
|||
expect(spectator.query('abp-layout-application')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should handle account layout from own property and display it', async () => { |
|||
spectator.router.navigateByUrl('/parentWithLayout/childWithLayout'); |
|||
await spectator.fixture.whenStable(); |
|||
await new Promise(resolve => setTimeout(resolve, 100)); |
|||
spectator.detectComponentChanges(); |
|||
expect(spectator.query('abp-layout-account')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should handle empty layout from route data and display it', async () => { |
|||
spectator.router.navigateByUrl('/withData'); |
|||
await spectator.fixture.whenStable(); |
|||
await new Promise(resolve => setTimeout(resolve, 100)); |
|||
spectator.detectComponentChanges(); |
|||
expect(spectator.query('abp-layout-empty')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should display empty layout when layout is null', async () => { |
|||
spectator.router.navigateByUrl('/withoutLayout'); |
|||
await spectator.fixture.whenStable(); |
|||
await new Promise(resolve => setTimeout(resolve, 100)); |
|||
spectator.detectComponentChanges(); |
|||
expect(spectator.query('abp-layout-empty')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should handle layout not found scenario', async () => { |
|||
spectator.router.navigateByUrl('/withoutLayout'); |
|||
await spectator.fixture.whenStable(); |
|||
await new Promise(resolve => setTimeout(resolve, 100)); |
|||
spectator.detectComponentChanges(); |
|||
|
|||
expect(spectator.query('abp-dynamic-layout')).toBeTruthy(); |
|||
}); |
|||
}); |
|||
|
|||
@ -1,282 +1,284 @@ |
|||
import { Injector } from '@angular/core'; |
|||
import { Router } from '@angular/router'; |
|||
import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest'; |
|||
import { BehaviorSubject } from 'rxjs'; |
|||
import { AbpApplicationConfigurationService } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service'; |
|||
import { TestBed } from '@angular/core/testing'; |
|||
import { of } from 'rxjs'; |
|||
import { LocalizationService } from '../services/localization.service'; |
|||
import { ConfigStateService } from '../services/config-state.service'; |
|||
import { SessionStateService } from '../services/session-state.service'; |
|||
import { LocalizationService } from '../services/localization.service'; |
|||
import { CORE_OPTIONS } from '../tokens/options.token'; |
|||
import { CONFIG_STATE_DATA } from './config-state.service.spec'; |
|||
import { AbpApplicationLocalizationService } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-localization.service'; |
|||
import { APPLICATION_LOCALIZATION_DATA } from './application-localization.service.spec'; |
|||
import { IncludeLocalizationResourcesProvider } from '../providers'; |
|||
|
|||
const appConfigData$ = new BehaviorSubject(CONFIG_STATE_DATA); |
|||
const appLocalizationData$ = new BehaviorSubject(APPLICATION_LOCALIZATION_DATA); |
|||
import { ABP } from '../models/common'; |
|||
|
|||
describe('LocalizationService', () => { |
|||
let spectator: SpectatorService<LocalizationService>; |
|||
let sessionState: SpyObject<SessionStateService>; |
|||
let configState: SpyObject<ConfigStateService>; |
|||
let service: LocalizationService; |
|||
let sessionState: SessionStateService; |
|||
let configState: ConfigStateService; |
|||
|
|||
const createService = createServiceFactory({ |
|||
service: LocalizationService, |
|||
entryComponents: [], |
|||
mocks: [Router], |
|||
providers: [ |
|||
IncludeLocalizationResourcesProvider, |
|||
{ |
|||
provide: CORE_OPTIONS, |
|||
useValue: { registerLocaleFn: () => Promise.resolve(), cultureNameLocaleFileMap: {} }, |
|||
const mockLocalizationData = { |
|||
defaultResourceName: 'MyProjectName', |
|||
values: { |
|||
MyProjectName: { |
|||
'Welcome': 'Welcome', |
|||
'Hello {0}': 'Hello {0}', |
|||
}, |
|||
{ |
|||
provide: AbpApplicationConfigurationService, |
|||
useValue: { get: () => appConfigData$ }, |
|||
AbpIdentity: { |
|||
'Identity': 'Identity', |
|||
'User': 'User', |
|||
}, |
|||
{ |
|||
provide: AbpApplicationLocalizationService, |
|||
useValue: { get: () => appLocalizationData$ }, |
|||
}, |
|||
], |
|||
}); |
|||
}, |
|||
}; |
|||
|
|||
const mockLocalizationsMap = new Map<string, Record<string, string>>(); |
|||
mockLocalizationsMap.set('AbpIdentity', { 'Identity': 'Identity', 'User': 'User' }); |
|||
mockLocalizationsMap.set('MyProjectName', { 'Welcome': 'Welcome', 'Hello {0}': 'Hello {0}' }); |
|||
|
|||
beforeEach(() => { |
|||
spectator = createService(); |
|||
sessionState = spectator.inject(SessionStateService); |
|||
configState = spectator.inject(ConfigStateService); |
|||
service = spectator.service; |
|||
TestBed.configureTestingModule({ |
|||
providers: [ |
|||
LocalizationService, |
|||
{ |
|||
provide: CORE_OPTIONS, |
|||
useValue: { |
|||
registerLocaleFn: () => Promise.resolve(), |
|||
cultureNameLocaleFileMap: {} |
|||
}, |
|||
}, |
|||
{ |
|||
provide: ConfigStateService, |
|||
useValue: { |
|||
refreshAppState: jest.fn(), |
|||
getDeep: jest.fn().mockReturnValue({ |
|||
localization: { |
|||
currentCulture: { cultureName: 'tr' }, |
|||
defaultResourceName: 'MyProjectName', |
|||
values: mockLocalizationData.values, |
|||
}, |
|||
}), |
|||
getDeep$: jest.fn().mockReturnValue(of({ |
|||
localization: { |
|||
currentCulture: { cultureName: 'tr' }, |
|||
defaultResourceName: 'MyProjectName', |
|||
values: mockLocalizationData.values, |
|||
}, |
|||
})), |
|||
getOne: jest.fn().mockReturnValue(mockLocalizationData), |
|||
getOne$: jest.fn().mockReturnValue(of(mockLocalizationData)), |
|||
getAll: jest.fn().mockReturnValue({ |
|||
localization: mockLocalizationData, |
|||
}), |
|||
getAll$: jest.fn().mockReturnValue(of({ |
|||
localization: mockLocalizationData, |
|||
})), |
|||
refreshLocalization: jest.fn().mockReturnValue(of({})), |
|||
}, |
|||
}, |
|||
{ |
|||
provide: SessionStateService, |
|||
useValue: { |
|||
setLanguage: jest.fn(), |
|||
setTenant: jest.fn(), |
|||
setInitialLanguage: jest.fn(), |
|||
getLanguage: jest.fn().mockReturnValue('tr'), |
|||
getTenant: jest.fn().mockReturnValue(null), |
|||
getInitialLanguage: jest.fn().mockReturnValue('tr'), |
|||
onLanguageChange$: jest.fn().mockReturnValue(of('tr')), |
|||
getLanguage$: jest.fn().mockReturnValue(of('tr')), |
|||
}, |
|||
}, |
|||
], |
|||
}); |
|||
service = TestBed.inject(LocalizationService); |
|||
sessionState = TestBed.inject(SessionStateService); |
|||
configState = TestBed.inject(ConfigStateService); |
|||
|
|||
configState.refreshAppState(); |
|||
sessionState.setLanguage('tr'); |
|||
appConfigData$.next(CONFIG_STATE_DATA); |
|||
(service as any).localizations$.next(mockLocalizationsMap); |
|||
}); |
|||
|
|||
describe('#currentLang', () => { |
|||
it('should be tr', done => { |
|||
setTimeout(() => { |
|||
expect(service.currentLang).toBe('tr'); |
|||
it('should return current language', () => { |
|||
expect(service.currentLang).toBe('tr'); |
|||
}); |
|||
|
|||
it('should return observable of current language', (done) => { |
|||
service.currentLang$.subscribe(lang => { |
|||
expect(lang).toBe('tr'); |
|||
done(); |
|||
}, 0); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('#languageChange$', () => { |
|||
it('should emit language changes', (done) => { |
|||
service.languageChange$.subscribe(lang => { |
|||
expect(lang).toBe('tr'); |
|||
done(); |
|||
}); |
|||
|
|||
(service as any)._languageChange$.next('tr'); |
|||
}); |
|||
}); |
|||
|
|||
describe('#get', () => { |
|||
it('should be return an observable localization', done => { |
|||
service.get('AbpIdentity::Identity').subscribe(localization => { |
|||
expect(localization).toBe(CONFIG_STATE_DATA.localization.values.AbpIdentity.Identity); |
|||
it('should return observable localization for valid key', (done) => { |
|||
service.get('AbpIdentity::Identity').subscribe(result => { |
|||
expect(result).toBe('Identity'); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return key when localization not found', (done) => { |
|||
service.get('AbpIdentity::NonExistent').subscribe(result => { |
|||
expect(result).toBe('NonExistent'); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should handle interpolation', (done) => { |
|||
service.get('MyProjectName::Hello {0}', 'John').subscribe(result => { |
|||
expect(result).toBe('Hello John'); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('#instant', () => { |
|||
it('should be return a localization', () => { |
|||
const localization = service.instant('AbpIdentity::Identity'); |
|||
it('should return localization for valid key', () => { |
|||
const result = service.instant('AbpIdentity::Identity'); |
|||
expect(result).toBe('Identity'); |
|||
}); |
|||
|
|||
expect(localization).toBe(CONFIG_STATE_DATA.localization.values.AbpIdentity.Identity); |
|||
it('should return key when localization not found', () => { |
|||
const result = service.instant('AbpIdentity::NonExistent'); |
|||
expect(result).toBe('NonExistent'); |
|||
}); |
|||
}); |
|||
|
|||
describe('#registerLocale', () => { |
|||
it('should throw an error message when service have an otherInstance', async () => { |
|||
try { |
|||
const instance = new LocalizationService( |
|||
sessionState, |
|||
spectator.inject(Injector), |
|||
null, |
|||
configState, |
|||
); |
|||
} catch (error) { |
|||
expect((error as Error).message).toBe('LocalizationService should have only one instance.'); |
|||
} |
|||
it('should handle interpolation', () => { |
|||
const result = service.instant('MyProjectName::Hello {0}', 'John'); |
|||
expect(result).toBe('Hello John'); |
|||
}); |
|||
}); |
|||
|
|||
describe('#localize', () => { |
|||
test.each` |
|||
resource | key | defaultValue | expected |
|||
${'_'} | ${'TEST'} | ${'DEFAULT'} | ${'TEST'} |
|||
${'foo'} | ${'bar'} | ${'DEFAULT'} | ${'baz'} |
|||
${'x'} | ${'bar'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'a'} | ${'bar'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${''} | ${'bar'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${undefined} | ${'bar'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'foo'} | ${'y'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'x'} | ${'y'} | ${'DEFAULT'} | ${'z'} |
|||
${'a'} | ${'y'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${''} | ${'y'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${undefined} | ${'y'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'foo'} | ${''} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'x'} | ${''} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'a'} | ${''} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${''} | ${''} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${undefined} | ${''} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'foo'} | ${undefined} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'x'} | ${undefined} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'a'} | ${undefined} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${''} | ${undefined} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${undefined} | ${undefined} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
`(
|
|||
'should return observable $expected when resource name is $resource and key is $key', |
|||
async ({ resource, key, defaultValue, expected }) => { |
|||
appConfigData$.next({ |
|||
localization: { |
|||
values: { foo: { bar: 'baz' }, x: { y: 'z' } }, |
|||
defaultResourceName: 'x', |
|||
}, |
|||
} as any); |
|||
configState.refreshAppState(); |
|||
it('should return observable localization for valid resource and key', (done) => { |
|||
service.localize('AbpIdentity', 'Identity', 'Default').subscribe(result => { |
|||
expect(result).toBe('Identity'); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
service.localize(resource, key, defaultValue).subscribe(result => { |
|||
expect(result).toBe(expected); |
|||
}); |
|||
}, |
|||
); |
|||
it('should return default value when key not found', (done) => { |
|||
service.localize('AbpIdentity', 'NonExistent', 'Default').subscribe(result => { |
|||
expect(result).toBe('Default'); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return default value when resource not found', (done) => { |
|||
service.localize('NonExistent', 'Identity', 'Default').subscribe(result => { |
|||
expect(result).toBe('Default'); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('#localizeSync', () => { |
|||
test.each` |
|||
resource | key | defaultValue | expected |
|||
${'_'} | ${'TEST'} | ${'DEFAULT'} | ${'TEST'} |
|||
${'foo'} | ${'bar'} | ${'DEFAULT'} | ${'baz'} |
|||
${'x'} | ${'bar'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'a'} | ${'bar'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${''} | ${'bar'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${undefined} | ${'bar'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'foo'} | ${'y'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'x'} | ${'y'} | ${'DEFAULT'} | ${'z'} |
|||
${'a'} | ${'y'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${''} | ${'y'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${undefined} | ${'y'} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'foo'} | ${''} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'x'} | ${''} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'a'} | ${''} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${''} | ${''} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${undefined} | ${''} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'foo'} | ${undefined} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'x'} | ${undefined} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${'a'} | ${undefined} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${''} | ${undefined} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${undefined} | ${undefined} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
`(
|
|||
'should return $expected when resource name is $resource and key is $key', |
|||
({ resource, key, defaultValue, expected }) => { |
|||
appConfigData$.next({ |
|||
localization: { |
|||
values: { foo: { bar: 'baz' }, x: { y: 'z' } }, |
|||
defaultResourceName: 'x', |
|||
}, |
|||
} as any); |
|||
configState.refreshAppState(); |
|||
it('should return localization for valid resource and key', () => { |
|||
const result = service.localizeSync('AbpIdentity', 'Identity', 'Default'); |
|||
expect(result).toBe('Identity'); |
|||
}); |
|||
|
|||
const result = service.localizeSync(resource, key, defaultValue); |
|||
it('should return default value when key not found', () => { |
|||
const result = service.localizeSync('AbpIdentity', 'NonExistent', 'Default'); |
|||
expect(result).toBe('Default'); |
|||
}); |
|||
|
|||
expect(result).toBe(expected); |
|||
}, |
|||
); |
|||
it('should return default value when resource not found', () => { |
|||
const result = service.localizeSync('NonExistent', 'Identity', 'Default'); |
|||
expect(result).toBe('Default'); |
|||
}); |
|||
}); |
|||
|
|||
describe('#localizeWithFallback', () => { |
|||
test.each` |
|||
resources | keys | defaultValue | expected |
|||
${['', '_']} | ${['TEST', 'OTHER']} | ${'DEFAULT'} | ${'TEST'} |
|||
${['foo']} | ${['bar']} | ${'DEFAULT'} | ${'baz'} |
|||
${['x']} | ${['bar']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['a', 'b', 'c']} | ${['bar']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['']} | ${['bar']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${[]} | ${['bar']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['foo']} | ${['y']} | ${'DEFAULT'} | ${'z'} |
|||
${['x']} | ${['y']} | ${'DEFAULT'} | ${'z'} |
|||
${['a', 'b', 'c']} | ${['y']} | ${'DEFAULT'} | ${'z'} |
|||
${['']} | ${['y']} | ${'DEFAULT'} | ${'z'} |
|||
${[]} | ${['y']} | ${'DEFAULT'} | ${'z'} |
|||
${['foo']} | ${['bar', 'y']} | ${'DEFAULT'} | ${'baz'} |
|||
${['x']} | ${['bar', 'y']} | ${'DEFAULT'} | ${'z'} |
|||
${['a', 'b', 'c']} | ${['bar', 'y']} | ${'DEFAULT'} | ${'z'} |
|||
${['']} | ${['bar', 'y']} | ${'DEFAULT'} | ${'z'} |
|||
${[]} | ${['bar', 'y']} | ${'DEFAULT'} | ${'z'} |
|||
${['foo']} | ${['']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['x']} | ${['']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['a', 'b', 'c']} | ${['']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['']} | ${['']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${[]} | ${['']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['foo']} | ${[]} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['x']} | ${[]} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['a', 'b', 'c']} | ${[]} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['']} | ${[]} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${[]} | ${[]} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
`(
|
|||
'should return observable $expected when resource names are $resources and keys are $keys', |
|||
async ({ resources, keys, defaultValue, expected }) => { |
|||
appConfigData$.next({ |
|||
localization: { |
|||
values: { foo: { bar: 'baz' }, x: { y: 'z' } }, |
|||
defaultResourceName: 'x', |
|||
}, |
|||
} as any); |
|||
configState.refreshAppState(); |
|||
it('should return observable localization from first available resource', (done) => { |
|||
service.localizeWithFallback(['AbpIdentity', 'MyProjectName'], ['Identity'], 'Default').subscribe(result => { |
|||
expect(result).toBe('Identity'); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
service.localizeWithFallback(resources, keys, defaultValue).subscribe(result => { |
|||
expect(result).toBe(expected); |
|||
}); |
|||
}, |
|||
); |
|||
it('should return default value when no resource has the key', (done) => { |
|||
service.localizeWithFallback(['AbpIdentity', 'MyProjectName'], ['NonExistent'], 'Default').subscribe(result => { |
|||
expect(result).toBe('Default'); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('#localizeWithFallbackSync', () => { |
|||
test.each` |
|||
resources | keys | defaultValue | expected |
|||
${['', '_']} | ${['TEST', 'OTHER']} | ${'DEFAULT'} | ${'TEST'} |
|||
${['foo']} | ${['bar']} | ${'DEFAULT'} | ${'baz'} |
|||
${['x']} | ${['bar']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['a', 'b', 'c']} | ${['bar']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['']} | ${['bar']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${[]} | ${['bar']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['foo']} | ${['y']} | ${'DEFAULT'} | ${'z'} |
|||
${['x']} | ${['y']} | ${'DEFAULT'} | ${'z'} |
|||
${['a', 'b', 'c']} | ${['y']} | ${'DEFAULT'} | ${'z'} |
|||
${['']} | ${['y']} | ${'DEFAULT'} | ${'z'} |
|||
${[]} | ${['y']} | ${'DEFAULT'} | ${'z'} |
|||
${['foo']} | ${['bar', 'y']} | ${'DEFAULT'} | ${'baz'} |
|||
${['x']} | ${['bar', 'y']} | ${'DEFAULT'} | ${'z'} |
|||
${['a', 'b', 'c']} | ${['bar', 'y']} | ${'DEFAULT'} | ${'z'} |
|||
${['']} | ${['bar', 'y']} | ${'DEFAULT'} | ${'z'} |
|||
${[]} | ${['bar', 'y']} | ${'DEFAULT'} | ${'z'} |
|||
${['foo']} | ${['']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['x']} | ${['']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['a', 'b', 'c']} | ${['']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['']} | ${['']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${[]} | ${['']} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['foo']} | ${[]} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['x']} | ${[]} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['a', 'b', 'c']} | ${[]} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${['']} | ${[]} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
${[]} | ${[]} | ${'DEFAULT'} | ${'DEFAULT'} |
|||
`(
|
|||
'should return $expected when resource names are $resources and keys are $keys', |
|||
({ resources, keys, defaultValue, expected }) => { |
|||
appConfigData$.next({ |
|||
localization: { |
|||
values: { foo: { bar: 'baz' }, x: { y: 'z' } }, |
|||
defaultResourceName: 'x', |
|||
}, |
|||
} as any); |
|||
configState.refreshAppState(); |
|||
it('should return localization from first available resource', () => { |
|||
const result = service.localizeWithFallbackSync(['AbpIdentity', 'MyProjectName'], ['Identity'], 'Default'); |
|||
expect(result).toBe('Identity'); |
|||
}); |
|||
|
|||
it('should return default value when no resource has the key', () => { |
|||
const result = service.localizeWithFallbackSync(['AbpIdentity', 'MyProjectName'], ['NonExistent'], 'Default'); |
|||
expect(result).toBe('Default'); |
|||
}); |
|||
}); |
|||
|
|||
const result = service.localizeWithFallbackSync(resources, keys, defaultValue); |
|||
describe('#getResource', () => { |
|||
it('should return resource for valid resource name', () => { |
|||
const resource = service.getResource('AbpIdentity'); |
|||
expect(resource).toBeDefined(); |
|||
expect(resource?.['Identity']).toBe('Identity'); |
|||
}); |
|||
|
|||
expect(result).toBe(expected); |
|||
}, |
|||
); |
|||
it('should return undefined for non-existent resource', () => { |
|||
const resource = service.getResource('NonExistent'); |
|||
expect(resource).toBeUndefined(); |
|||
}); |
|||
}); |
|||
|
|||
describe('#getLocalization', () => { |
|||
it('should return a localization', () => { |
|||
expect( |
|||
service.instant("MyProjectName::'{0}' and '{1}' do not match.", 'first', 'second'), |
|||
).toBe('first and second do not match.'); |
|||
describe('#getResource$', () => { |
|||
it('should return observable resource for valid resource name', (done) => { |
|||
service.getResource$('AbpIdentity').subscribe(resource => { |
|||
expect(resource).toBeDefined(); |
|||
expect(resource?.['Identity']).toBe('Identity'); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
it('should return observable undefined for non-existent resource', (done) => { |
|||
service.getResource$('NonExistent').subscribe(resource => { |
|||
expect(resource).toBeUndefined(); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('#addLocalization', () => { |
|||
it('should add localization data', () => { |
|||
const localizations: ABP.Localization[] = [ |
|||
{ |
|||
culture: 'en', |
|||
resources: [ |
|||
{ |
|||
resourceName: 'TestResource', |
|||
texts: { |
|||
'TestKey': 'TestValue', |
|||
}, |
|||
}, |
|||
], |
|||
}, |
|||
]; |
|||
|
|||
service.addLocalization(localizations); |
|||
|
|||
expect(() => service.addLocalization(localizations)).not.toThrow(); |
|||
}); |
|||
}); |
|||
|
|||
describe('#registerLocale', () => { |
|||
it('should register locale successfully', async () => { |
|||
const result = await service.registerLocale('en'); |
|||
expect(result).toBeUndefined(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
@ -1,174 +1,176 @@ |
|||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; |
|||
import { Router } from '@angular/router'; |
|||
import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; |
|||
import { BehaviorSubject } from 'rxjs'; |
|||
import { ReplaceableTemplateDirective } from '../directives/replaceable-template.directive'; |
|||
import { ReplaceableComponents } from '../models/replaceable-components'; |
|||
import { ReplaceableComponentsService } from '../services/replaceable-components.service'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-default-component', |
|||
template: ' <p>default</p> ', |
|||
exportAs: 'abpDefaultComponent', |
|||
}) |
|||
class DefaultComponent { |
|||
@Input() |
|||
oneWay; |
|||
|
|||
@Input() |
|||
twoWay: boolean; |
|||
|
|||
@Output() |
|||
readonly twoWayChange = new EventEmitter<boolean>(); |
|||
|
|||
@Output() |
|||
readonly someOutput = new EventEmitter<string>(); |
|||
|
|||
setTwoWay(value) { |
|||
this.twoWay = value; |
|||
this.twoWayChange.emit(value); |
|||
} |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'abp-external-component', |
|||
template: ' <p>external</p> ', |
|||
}) |
|||
class ExternalComponent {
data = inject<ReplaceableComponents.ReplaceableTemplateData<any, any>>('REPLACEABLE_DATA' as any, { optional: true })!; |
|||
|
|||
} |
|||
|
|||
describe('ReplaceableTemplateDirective', () => { |
|||
let spectator: SpectatorDirective<ReplaceableTemplateDirective>; |
|||
const get$Res = new BehaviorSubject(undefined); |
|||
|
|||
const createDirective = createDirectiveFactory({ |
|||
directive: ReplaceableTemplateDirective, |
|||
declarations: [DefaultComponent, ExternalComponent], |
|||
entryComponents: [ExternalComponent], |
|||
mocks: [Router], |
|||
providers: [{ provide: ReplaceableComponentsService, useValue: { get$: () => get$Res } }], |
|||
}); |
|||
|
|||
describe('without external component', () => { |
|||
const twoWayChange = jest.fn(a => a); |
|||
const someOutput = jest.fn(a => a); |
|||
|
|||
beforeEach(() => { |
|||
spectator = createDirective( |
|||
` |
|||
<div *abpReplaceableTemplate="{inputs: {oneWay: {value: oneWay}, twoWay: {value: twoWay, twoWay: true}}, outputs: {twoWayChange: twoWayChange, someOutput: someOutput}, componentKey: 'TestModule.TestComponent'}; let initTemplate = initTemplate"> |
|||
<abp-default-component #defaultComponent="abpDefaultComponent"></abp-default-component> |
|||
</div> |
|||
`,
|
|||
{ |
|||
hostProps: { |
|||
oneWay: { label: 'Test' }, |
|||
twoWay: false, |
|||
twoWayChange, |
|||
someOutput, |
|||
}, |
|||
}, |
|||
); |
|||
|
|||
const component = spectator.query(DefaultComponent); |
|||
spectator.directive.context.initTemplate(component); |
|||
spectator.detectChanges(); |
|||
}); |
|||
|
|||
afterEach(() => twoWayChange.mockClear()); |
|||
|
|||
it('should display the default template when store response is undefined', () => { |
|||
expect(spectator.query('abp-default-component')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should be setted inputs and outputs', () => { |
|||
const component = spectator.query(DefaultComponent); |
|||
expect(component.oneWay).toEqual({ label: 'Test' }); |
|||
expect(component.twoWay).toEqual(false); |
|||
}); |
|||
|
|||
it('should change the component inputs', () => { |
|||
const component = spectator.query(DefaultComponent); |
|||
spectator.setHostInput({ oneWay: 'test' }); |
|||
component.setTwoWay(true); |
|||
component.someOutput.emit('someOutput emitted'); |
|||
expect(component.oneWay).toBe('test'); |
|||
expect(twoWayChange).toHaveBeenCalledWith(true); |
|||
expect(someOutput).toHaveBeenCalledWith('someOutput emitted'); |
|||
}); |
|||
}); |
|||
|
|||
describe('with external component', () => { |
|||
const twoWayChange = jest.fn(a => a); |
|||
const someOutput = jest.fn(a => a); |
|||
|
|||
beforeEach(() => { |
|||
spectator = createDirective( |
|||
` |
|||
<div *abpReplaceableTemplate="{inputs: {oneWay: {value: oneWay}, twoWay: {value: twoWay, twoWay: true}}, outputs: {twoWayChange: twoWayChange, someOutput: someOutput}, componentKey: 'TestModule.TestComponent'}; let initTemplate = initTemplate"> |
|||
<abp-default-component #defaultComponent="abpDefaultComponent"></abp-default-component> |
|||
</div> |
|||
`,
|
|||
{ hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } }, |
|||
); |
|||
|
|||
get$Res.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); |
|||
}); |
|||
|
|||
afterEach(() => twoWayChange.mockClear()); |
|||
|
|||
it('should display the external component', () => { |
|||
expect(spectator.query('p')).toHaveText('external'); |
|||
}); |
|||
|
|||
it('should be injected the data object', () => { |
|||
const externalComponent = spectator.query(ExternalComponent); |
|||
expect(externalComponent.data).toEqual({ |
|||
componentKey: 'TestModule.TestComponent', |
|||
inputs: { oneWay: { label: 'Test' }, twoWay: false }, |
|||
outputs: { someOutput, twoWayChange }, |
|||
}); |
|||
}); |
|||
|
|||
it('should be worked all data properties', () => { |
|||
const externalComponent = spectator.query(ExternalComponent); |
|||
spectator.setHostInput({ oneWay: 'test' }); |
|||
externalComponent.data.inputs.twoWay = true; |
|||
externalComponent.data.outputs.someOutput('someOutput emitted'); |
|||
expect(externalComponent.data.inputs.oneWay).toBe('test'); |
|||
expect(twoWayChange).toHaveBeenCalledWith(true); |
|||
expect(someOutput).toHaveBeenCalledWith('someOutput emitted'); |
|||
|
|||
spectator.setHostInput({ twoWay: 'twoWay test' }); |
|||
expect(externalComponent.data.inputs.twoWay).toBe('twoWay test'); |
|||
}); |
|||
|
|||
it('should be worked correctly the default component when the external component has been removed from store', () => { |
|||
expect(spectator.query('p')).toHaveText('external'); |
|||
const externalComponent = spectator.query(ExternalComponent); |
|||
spectator.setHostInput({ oneWay: 'test' }); |
|||
externalComponent.data.inputs.twoWay = true; |
|||
get$Res.next({ component: null, key: 'TestModule.TestComponent' }); |
|||
spectator.detectChanges(); |
|||
const component = spectator.query(DefaultComponent); |
|||
spectator.directive.context.initTemplate(component); |
|||
expect(spectator.query('abp-default-component')).toBeTruthy(); |
|||
|
|||
expect(component.oneWay).toEqual('test'); |
|||
expect(component.twoWay).toEqual(true); |
|||
}); |
|||
|
|||
it('should reset default component subscriptions', () => { |
|||
get$Res.next({ component: null, key: 'TestModule.TestComponent' }); |
|||
const component = spectator.query(DefaultComponent); |
|||
spectator.directive.context.initTemplate(component); |
|||
spectator.detectChanges(); |
|||
const unsubscribe = jest.fn(() => {}); |
|||
spectator.directive.defaultComponentSubscriptions.twoWayChange.unsubscribe = unsubscribe; |
|||
|
|||
get$Res.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); |
|||
expect(unsubscribe).toHaveBeenCalled(); |
|||
}); |
|||
}); |
|||
}); |
|||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; |
|||
import { Router } from '@angular/router'; |
|||
import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; |
|||
import { BehaviorSubject } from 'rxjs'; |
|||
import { ReplaceableTemplateDirective } from '../directives/replaceable-template.directive'; |
|||
import { ReplaceableComponents } from '../models/replaceable-components'; |
|||
import { ReplaceableComponentsService } from '../services/replaceable-components.service'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-default-component', |
|||
template: ' <p>default</p> ', |
|||
exportAs: 'abpDefaultComponent', |
|||
standalone: true, |
|||
}) |
|||
class DefaultComponent { |
|||
@Input() |
|||
oneWay; |
|||
|
|||
@Input() |
|||
twoWay: boolean; |
|||
|
|||
@Output() |
|||
readonly twoWayChange = new EventEmitter<boolean>(); |
|||
|
|||
@Output() |
|||
readonly someOutput = new EventEmitter<string>(); |
|||
|
|||
setTwoWay(value) { |
|||
this.twoWay = value; |
|||
this.twoWayChange.emit(value); |
|||
} |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'abp-external-component', |
|||
template: ' <p>external</p> ', |
|||
standalone: true, |
|||
}) |
|||
class ExternalComponent { |
|||
data = inject<ReplaceableComponents.ReplaceableTemplateData<any, any>>('REPLACEABLE_DATA' as any, { optional: true })!; |
|||
|
|||
} |
|||
|
|||
describe('ReplaceableTemplateDirective', () => { |
|||
let spectator: SpectatorDirective<ReplaceableTemplateDirective>; |
|||
const get$Res = new BehaviorSubject(undefined); |
|||
|
|||
const createDirective = createDirectiveFactory({ |
|||
directive: ReplaceableTemplateDirective, |
|||
imports: [DefaultComponent, ExternalComponent], |
|||
mocks: [Router], |
|||
providers: [{ provide: ReplaceableComponentsService, useValue: { get$: () => get$Res } }], |
|||
}); |
|||
|
|||
describe('without external component', () => { |
|||
const twoWayChange = jest.fn(a => a); |
|||
const someOutput = jest.fn(a => a); |
|||
|
|||
beforeEach(() => { |
|||
spectator = createDirective( |
|||
` |
|||
<div *abpReplaceableTemplate="{inputs: {oneWay: {value: oneWay}, twoWay: {value: twoWay, twoWay: true}}, outputs: {twoWayChange: twoWayChange, someOutput: someOutput}, componentKey: 'TestModule.TestComponent'}; let initTemplate = initTemplate"> |
|||
<abp-default-component #defaultComponent="abpDefaultComponent"></abp-default-component> |
|||
</div> |
|||
`,
|
|||
{ |
|||
hostProps: { |
|||
oneWay: { label: 'Test' }, |
|||
twoWay: false, |
|||
twoWayChange, |
|||
someOutput, |
|||
}, |
|||
}, |
|||
); |
|||
|
|||
const component = spectator.query(DefaultComponent); |
|||
spectator.directive.context.initTemplate(component); |
|||
spectator.detectChanges(); |
|||
}); |
|||
|
|||
afterEach(() => twoWayChange.mockClear()); |
|||
|
|||
it('should display the default template when store response is undefined', () => { |
|||
expect(spectator.query('abp-default-component')).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should be setted inputs and outputs', () => { |
|||
const component = spectator.query(DefaultComponent); |
|||
expect(component.oneWay).toEqual({ label: 'Test' }); |
|||
expect(component.twoWay).toEqual(false); |
|||
}); |
|||
|
|||
it('should change the component inputs', () => { |
|||
const component = spectator.query(DefaultComponent); |
|||
spectator.setHostInput({ oneWay: 'test' }); |
|||
component.setTwoWay(true); |
|||
component.someOutput.emit('someOutput emitted'); |
|||
expect(component.oneWay).toBe('test'); |
|||
expect(twoWayChange).toHaveBeenCalledWith(true); |
|||
expect(someOutput).toHaveBeenCalledWith('someOutput emitted'); |
|||
}); |
|||
}); |
|||
|
|||
describe('with external component', () => { |
|||
const twoWayChange = jest.fn(a => a); |
|||
const someOutput = jest.fn(a => a); |
|||
|
|||
beforeEach(() => { |
|||
spectator = createDirective( |
|||
` |
|||
<div *abpReplaceableTemplate="{inputs: {oneWay: {value: oneWay}, twoWay: {value: twoWay, twoWay: true}}, outputs: {twoWayChange: twoWayChange, someOutput: someOutput}, componentKey: 'TestModule.TestComponent'}; let initTemplate = initTemplate"> |
|||
<abp-default-component #defaultComponent="abpDefaultComponent"></abp-default-component> |
|||
</div> |
|||
`,
|
|||
{ hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } }, |
|||
); |
|||
|
|||
get$Res.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); |
|||
}); |
|||
|
|||
afterEach(() => twoWayChange.mockClear()); |
|||
|
|||
it('should display the external component', () => { |
|||
expect(spectator.query('p')).toHaveText('external'); |
|||
}); |
|||
|
|||
it('should be injected the data object', () => { |
|||
const externalComponent = spectator.query(ExternalComponent); |
|||
expect(externalComponent.data).toEqual({ |
|||
componentKey: 'TestModule.TestComponent', |
|||
inputs: { oneWay: { label: 'Test' }, twoWay: false }, |
|||
outputs: { someOutput, twoWayChange }, |
|||
}); |
|||
}); |
|||
|
|||
it('should be worked all data properties', () => { |
|||
const externalComponent = spectator.query(ExternalComponent); |
|||
spectator.setHostInput({ oneWay: 'test' }); |
|||
externalComponent.data.inputs.twoWay = true; |
|||
externalComponent.data.outputs.someOutput('someOutput emitted'); |
|||
expect(externalComponent.data.inputs.oneWay).toBe('test'); |
|||
expect(twoWayChange).toHaveBeenCalledWith(true); |
|||
expect(someOutput).toHaveBeenCalledWith('someOutput emitted'); |
|||
|
|||
spectator.setHostInput({ twoWay: 'twoWay test' }); |
|||
expect(externalComponent.data.inputs.twoWay).toBe('twoWay test'); |
|||
}); |
|||
|
|||
it('should be worked correctly the default component when the external component has been removed from store', () => { |
|||
expect(spectator.query('p')).toHaveText('external'); |
|||
const externalComponent = spectator.query(ExternalComponent); |
|||
spectator.setHostInput({ oneWay: 'test' }); |
|||
externalComponent.data.inputs.twoWay = true; |
|||
get$Res.next({ component: null, key: 'TestModule.TestComponent' }); |
|||
spectator.detectChanges(); |
|||
const component = spectator.query(DefaultComponent); |
|||
spectator.directive.context.initTemplate(component); |
|||
expect(spectator.query('abp-default-component')).toBeTruthy(); |
|||
|
|||
expect(component.oneWay).toEqual('test'); |
|||
expect(component.twoWay).toEqual(true); |
|||
}); |
|||
|
|||
it('should reset default component subscriptions', () => { |
|||
get$Res.next({ component: null, key: 'TestModule.TestComponent' }); |
|||
const component = spectator.query(DefaultComponent); |
|||
spectator.directive.context.initTemplate(component); |
|||
spectator.detectChanges(); |
|||
const unsubscribe = jest.fn(() => {}); |
|||
spectator.directive.defaultComponentSubscriptions.twoWayChange.unsubscribe = unsubscribe; |
|||
|
|||
get$Res.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); |
|||
expect(unsubscribe).toHaveBeenCalled(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
Loading…
Reference in new issue