Browse Source

Merge pull request #3698 from abpframework/feat/3139

Increased Flexibility and Maintainability of CoreModule
pull/3719/head
Mehmet Erim 6 years ago
committed by GitHub
parent
commit
a5988d0f25
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 114
      npm/ng-packs/packages/core/src/lib/core.module.ts
  2. 8
      npm/ng-packs/packages/core/src/lib/localization.module.ts
  3. 5
      npm/ng-packs/packages/core/src/lib/models/common.ts
  4. 22
      npm/ng-packs/packages/core/src/lib/pipes/localization.pipe.ts
  5. 25
      npm/ng-packs/packages/core/src/lib/tests/config.plugin.spec.ts
  6. 2
      npm/ng-packs/packages/core/src/public-api.ts
  7. 8
      npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.ts
  8. 125
      npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts
  9. 155
      npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts

114
npm/ng-packs/packages/core/src/lib/core.module.ts

@ -1,4 +1,4 @@
import { CommonModule } from '@angular/common';
import { APP_BASE_HREF, CommonModule } from '@angular/common';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { APP_INITIALIZER, Injector, ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@ -22,8 +22,9 @@ import { ReplaceableTemplateDirective } from './directives/replaceable-template.
import { StopPropagationDirective } from './directives/stop-propagation.directive';
import { VisibilityDirective } from './directives/visibility.directive';
import { ApiInterceptor } from './interceptors/api.interceptor';
import { LocalizationModule } from './localization.module';
import { ABP } from './models/common';
import { LocalizationPipe } from './pipes/localization.pipe';
import { LocalizationPipe, MockLocalizationPipe } from './pipes/localization.pipe';
import { SortPipe } from './pipes/sort.pipe';
import { ConfigPlugin, NGXS_CONFIG_PLUGIN_OPTIONS } from './plugins/config.plugin';
import { LocaleProvider } from './providers/locale.provider';
@ -32,77 +33,128 @@ import { ProfileState } from './states/profile.state';
import { ReplaceableComponentsState } from './states/replaceable-components.state';
import { SessionState } from './states/session.state';
import { CORE_OPTIONS } from './tokens/options.token';
import { getInitialData, localeInitializer } from './utils/initial-utils';
import './utils/date-extensions';
import { getInitialData, localeInitializer } from './utils/initial-utils';
export function storageFactory(): OAuthStorage {
return localStorage;
}
/**
* BaseCoreModule is the module that holds
* all imports, declarations, exports, and entryComponents
* but not the providers.
* This module will be imported and exported by all others.
*/
@NgModule({
imports: [
NgxsModule.forFeature([ReplaceableComponentsState, ProfileState, SessionState, ConfigState]),
NgxsRouterPluginModule.forRoot(),
NgxsStoragePluginModule.forRoot({ key: ['SessionState'] }),
OAuthModule,
exports: [
CommonModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
RouterModule,
],
declarations: [
ReplaceableRouteContainerComponent,
RouterOutletComponent,
DynamicLayoutComponent,
AbstractNgModelComponent,
AutofocusDirective,
DynamicLayoutComponent,
EllipsisDirective,
ForDirective,
FormSubmitDirective,
LocalizationPipe,
SortPipe,
InitDirective,
PermissionDirective,
VisibilityDirective,
InputEventDebounceDirective,
StopPropagationDirective,
PermissionDirective,
ReplaceableRouteContainerComponent,
ReplaceableTemplateDirective,
AbstractNgModelComponent,
RouterOutletComponent,
SortPipe,
StopPropagationDirective,
VisibilityDirective,
],
exports: [
imports: [
OAuthModule,
CommonModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
RouterModule,
RouterOutletComponent,
DynamicLayoutComponent,
],
declarations: [
AbstractNgModelComponent,
ReplaceableRouteContainerComponent,
AutofocusDirective,
DynamicLayoutComponent,
EllipsisDirective,
ForDirective,
FormSubmitDirective,
InitDirective,
PermissionDirective,
VisibilityDirective,
InputEventDebounceDirective,
PermissionDirective,
ReplaceableRouteContainerComponent,
ReplaceableTemplateDirective,
StopPropagationDirective,
LocalizationPipe,
RouterOutletComponent,
SortPipe,
LocalizationPipe,
StopPropagationDirective,
VisibilityDirective,
],
providers: [LocalizationPipe],
entryComponents: [
RouterOutletComponent,
DynamicLayoutComponent,
ReplaceableRouteContainerComponent,
],
})
export class BaseCoreModule {}
/**
* RootCoreModule is the module that will be used at root level
* and it introduces imports useful at root level (e.g. NGXS)
*/
@NgModule({
exports: [BaseCoreModule, LocalizationModule],
imports: [
BaseCoreModule,
LocalizationModule,
NgxsModule.forFeature([ReplaceableComponentsState, ProfileState, SessionState, ConfigState]),
NgxsRouterPluginModule.forRoot(),
NgxsStoragePluginModule.forRoot({ key: ['SessionState'] }),
],
})
export class RootCoreModule {}
/**
* TestCoreModule is the module that will be used in tests
* and it provides mock alternatives
*/
@NgModule({
exports: [RouterModule, BaseCoreModule, MockLocalizationPipe],
imports: [RouterModule.forRoot([]), BaseCoreModule],
declarations: [MockLocalizationPipe],
})
export class TestCoreModule {}
/**
* CoreModule is the module that is publicly available
*/
@NgModule({
exports: [BaseCoreModule, LocalizationModule],
imports: [BaseCoreModule, LocalizationModule],
providers: [LocalizationPipe],
})
export class CoreModule {
static forRoot(options = {} as ABP.Root): ModuleWithProviders {
static forTest({ baseHref = '/' } = {} as ABP.Test): ModuleWithProviders<TestCoreModule> {
return {
ngModule: TestCoreModule,
providers: [
{ provide: APP_BASE_HREF, useValue: baseHref },
{
provide: LocalizationPipe,
useClass: MockLocalizationPipe,
},
],
};
}
static forRoot(options = {} as ABP.Root): ModuleWithProviders<RootCoreModule> {
return {
ngModule: CoreModule,
ngModule: RootCoreModule,
providers: [
LocaleProvider,
{

8
npm/ng-packs/packages/core/src/lib/localization.module.ts

@ -0,0 +1,8 @@
import { NgModule } from '@angular/core';
import { LocalizationPipe } from './pipes/localization.pipe';
@NgModule({
exports: [LocalizationPipe],
declarations: [LocalizationPipe],
})
export class LocalizationModule {}

5
npm/ng-packs/packages/core/src/lib/models/common.ts

@ -1,4 +1,5 @@
import { EventEmitter } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs';
import { eLayoutType } from '../enums/common';
import { Config } from './config';
@ -14,6 +15,10 @@ export namespace ABP {
skipGetAppConfiguration?: boolean;
}
export interface Test {
baseHref?: Router;
}
export type PagedResponse<T> = {
totalCount: number;
} & PagedItemsResponse<T>;

22
npm/ng-packs/packages/core/src/lib/pipes/localization.pipe.ts

@ -1,4 +1,4 @@
import { Pipe, PipeTransform, Injectable } from '@angular/core';
import { Injectable, Pipe, PipeTransform } from '@angular/core';
import { Store } from '@ngxs/store';
import { Config } from '../models';
import { ConfigState } from '../states';
@ -10,12 +10,28 @@ import { ConfigState } from '../states';
export class LocalizationPipe implements PipeTransform {
constructor(private store: Store) {}
transform(value: string | Config.LocalizationWithDefault = '', ...interpolateParams: string[]): string {
transform(
value: string | Config.LocalizationWithDefault = '',
...interpolateParams: string[]
): string {
return this.store.selectSnapshot(
ConfigState.getLocalization(
value,
...interpolateParams.reduce((acc, val) => (Array.isArray(val) ? [...acc, ...val] : [...acc, val]), []),
...interpolateParams.reduce(
(acc, val) => (Array.isArray(val) ? [...acc, ...val] : [...acc, val]),
[],
),
),
);
}
}
@Injectable()
@Pipe({
name: 'abpLocalization',
})
export class MockLocalizationPipe implements PipeTransform {
transform(value: string | Config.LocalizationWithDefault = '', ..._: string[]) {
return typeof value === 'string' ? value : value.defaultValue;
}
}

25
npm/ng-packs/packages/core/src/lib/tests/config.plugin.spec.ts

@ -1,16 +1,15 @@
import { RouterTestingModule } from '@angular/router/testing';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { NgxsModule, NGXS_PLUGINS, Store } from '@ngxs/store';
import { NgxsModule, Store } from '@ngxs/store';
import { OAuthModule } from 'angular-oauth2-oidc';
import { environment } from '../../../../../apps/dev-app/src/environments/environment';
import { LAYOUTS } from '@abp/ng.theme.basic';
import { RouterOutletComponent } from '../components';
import { CoreModule } from '../core.module';
import { eLayoutType } from '../enums/common';
import { ABP } from '../models';
import { ConfigPlugin, NGXS_CONFIG_PLUGIN_OPTIONS } from '../plugins';
import { ConfigPlugin } from '../plugins';
import { ConfigState } from '../states';
import { addAbpRoutes } from '../utils';
import { OAuthModule } from 'angular-oauth2-oidc';
addAbpRoutes([
{
@ -60,9 +59,6 @@ addAbpRoutes([
const expectedState = {
environment,
requirements: {
layouts: LAYOUTS,
},
routes: [
{
name: '::Menu:Home',
@ -323,9 +319,9 @@ describe('ConfigPlugin', () => {
const createService = createServiceFactory({
service: ConfigPlugin,
imports: [
CoreModule,
NgxsModule.forRoot([ConfigState]),
CoreModule.forRoot({ environment }),
OAuthModule.forRoot(),
NgxsModule.forRoot([]),
RouterTestingModule.withRoutes([
{
path: '',
@ -341,17 +337,6 @@ describe('ConfigPlugin', () => {
{ path: 'tenant-management', component: RouterOutletComponent },
]),
],
providers: [
{
provide: NGXS_PLUGINS,
useClass: ConfigPlugin,
multi: true,
},
{
provide: NGXS_CONFIG_PLUGIN_OPTIONS,
useValue: { environment, requirements: { layouts: LAYOUTS } } as ABP.Root,
},
],
});
beforeEach(() => {

2
npm/ng-packs/packages/core/src/public-api.ts

@ -7,7 +7,7 @@ export * from './lib/abstracts';
export * from './lib/actions';
export * from './lib/components';
export * from './lib/constants';
export * from './lib/core.module';
export { CoreModule } from './lib/core.module';
export * from './lib/directives';
export * from './lib/enums';
export * from './lib/guards';

8
npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.ts

@ -1,7 +1,6 @@
import { Component } from '@angular/core';
import { ConfirmationService } from '../../services/confirmation.service';
import { Confirmation } from '../../models/confirmation';
import { LocalizationService } from '@abp/ng.core';
import { ConfirmationService } from '../../services/confirmation.service';
@Component({
selector: 'abp-confirmation',
@ -32,10 +31,7 @@ export class ConfirmationComponent {
}
}
constructor(
private confirmationService: ConfirmationService,
private localizationService: LocalizationService,
) {
constructor(private confirmationService: ConfirmationService) {
this.confirmationService.confirmation$.subscribe(confirmation => {
this.data = confirmation;
this.visible = !!confirmation;

125
npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts

@ -1,74 +1,101 @@
import { CoreModule } from '@abp/ng.core';
import { Component } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { NgModule } from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { NgxsModule } from '@ngxs/store';
import { ConfirmationService } from '../services/confirmation.service';
import { ThemeSharedModule } from '../theme-shared.module';
import { OAuthModule, OAuthService } from 'angular-oauth2-oidc';
@Component({
selector: 'abp-dummy',
template: `
<abp-confirmation></abp-confirmation>
`,
import { timer } from 'rxjs';
import { take } from 'rxjs/operators';
import { ConfirmationComponent } from '../components';
import { Confirmation } from '../models';
import { ConfirmationService } from '../services';
@NgModule({
exports: [ConfirmationComponent],
entryComponents: [ConfirmationComponent],
declarations: [ConfirmationComponent],
imports: [CoreModule.forTest()],
})
class DummyComponent {
constructor(public confirmation: ConfirmationService) {}
}
export class MockModule {}
describe('ConfirmationService', () => {
let spectator: Spectator<DummyComponent>;
let spectator: SpectatorService<ConfirmationService>;
let service: ConfirmationService;
const createComponent = createComponentFactory({
component: DummyComponent,
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot(), RouterTestingModule],
mocks: [OAuthService],
const createService = createServiceFactory({
service: ConfirmationService,
imports: [NgxsModule.forRoot(), CoreModule.forTest(), MockModule],
});
beforeEach(() => {
spectator = createComponent();
service = spectator.get(ConfirmationService);
spectator = createService();
service = spectator.service;
});
afterEach(() => {
clearElements();
});
test.skip('should display a confirmation popup', () => {
service.info('test', 'title');
test('should display a confirmation popup', fakeAsync(() => {
service.show('MESSAGE', 'TITLE');
spectator.detectChanges();
tick();
expect(spectator.query('div.confirmation .title')).toHaveText('title');
expect(spectator.query('div.confirmation .message')).toHaveText('test');
expect(selectConfirmationContent('.title')).toBe('TITLE');
expect(selectConfirmationContent('.message')).toBe('MESSAGE');
}));
test.each`
type | selector | icon
${'info'} | ${'.info'} | ${'.fa-info-circle'}
${'success'} | ${'.success'} | ${'.fa-check-circle'}
${'warn'} | ${'.warning'} | ${'.fa-exclamation-triangle'}
${'error'} | ${'.error'} | ${'.fa-times-circle'}
`('should display $type confirmation popup', async ({ type, selector, icon }) => {
service[type]('MESSAGE', 'TITLE');
await timer(0).toPromise();
expect(selectConfirmationContent('.title')).toBe('TITLE');
expect(selectConfirmationContent('.message')).toBe('MESSAGE');
expect(selectConfirmationElement(selector)).toBeTruthy();
expect(selectConfirmationElement(icon)).toBeTruthy();
});
test.skip('should close with ESC key', done => {
service.info('test', 'title').subscribe(() => {
setTimeout(() => {
spectator.detectComponentChanges();
expect(spectator.query('div.confirmation')).toBeFalsy();
test('should close with ESC key', done => {
service
.info('', '')
.pipe(take(1))
.subscribe(status => {
expect(status).toBe(Confirmation.Status.dismiss);
done();
}, 0);
});
});
spectator.detectChanges();
expect(spectator.query('div.confirmation')).toBeTruthy();
spectator.dispatchKeyboardEvent('div.confirmation', 'keyup', 'Escape');
const escape = new KeyboardEvent('keyup', { key: 'Escape' });
document.dispatchEvent(escape);
});
test.skip('should close when click cancel button', done => {
service.info('test', 'title', { yesText: 'Sure', cancelText: 'Exit' }).subscribe(() => {
spectator.detectComponentChanges();
setTimeout(() => {
expect(spectator.query('div.confirmation')).toBeFalsy();
done();
}, 0);
test('should close when click cancel button', async done => {
service.info('', '', { yesText: 'Sure', cancelText: 'Exit' }).subscribe(status => {
expect(status).toBe(Confirmation.Status.reject);
done();
});
spectator.detectChanges();
await timer(0).toPromise();
expect(spectator.query('div.confirmation')).toBeTruthy();
expect(spectator.query('button#cancel')).toHaveText('Exit');
expect(spectator.query('button#confirm')).toHaveText('Sure');
expect(selectConfirmationContent('button#cancel')).toBe('Exit');
expect(selectConfirmationContent('button#confirm')).toBe('Sure');
spectator.click('button#cancel');
selectConfirmationElement<HTMLButtonElement>('button#cancel').click();
});
});
function clearElements(selector = '.confirmation') {
document.querySelectorAll(selector).forEach(element => element.parentNode.removeChild(element));
}
function selectConfirmationContent(selector = '.confirmation'): string {
return selectConfirmationElement(selector).textContent.trim();
}
function selectConfirmationElement<T extends HTMLElement>(selector = '.confirmation'): T {
return document.querySelector(selector);
}

155
npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts

@ -1,87 +1,122 @@
import { CoreModule } from '@abp/ng.core';
import { Component } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { NgModule } from '@angular/core';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { NgxsModule } from '@ngxs/store';
import { timer } from 'rxjs';
import { ToastContainerComponent } from '../components/toast-container/toast-container.component';
import { ToastComponent } from '../components/toast/toast.component';
import { ToasterService } from '../services/toaster.service';
import { ThemeSharedModule } from '../theme-shared.module';
import { OAuthService } from 'angular-oauth2-oidc';
@Component({
selector: 'abp-dummy',
template: `
<abp-toast-container></abp-toast-container>
`,
@NgModule({
exports: [ToastContainerComponent],
entryComponents: [ToastContainerComponent],
declarations: [ToastContainerComponent, ToastComponent],
imports: [CoreModule.forTest()],
})
class DummyComponent {
constructor(public toaster: ToasterService) {}
}
export class MockModule {}
describe('ToasterService', () => {
let spectator: Spectator<DummyComponent>;
let spectator: SpectatorService<ToasterService>;
let service: ToasterService;
const createComponent = createComponentFactory({
component: DummyComponent,
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot(), RouterTestingModule],
mocks: [OAuthService],
const createService = createServiceFactory({
service: ToasterService,
imports: [NgxsModule.forRoot(), CoreModule.forTest(), MockModule],
});
beforeEach(() => {
spectator = createComponent();
service = spectator.get(ToasterService);
spectator = createService();
service = spectator.service;
});
afterEach(() => {
clearElements();
});
test.skip('should display an error toast', () => {
service.error('test', 'title');
test('should display a toast', async () => {
service.show('MESSAGE', 'TITLE');
spectator.detectChanges();
await timer(0).toPromise();
service['containerComponentRef'].changeDetectorRef.detectChanges();
expect(spectator.query('div.toast')).toBeTruthy();
expect(spectator.query('.toast-icon i')).toHaveClass('fa-times-circle');
expect(spectator.query('div.toast-title')).toHaveText('title');
expect(spectator.query('p.toast-message')).toHaveText('test');
expect(selectToasterElement('.fa-exclamation-circle')).toBeTruthy();
expect(selectToasterContent('.toast-title')).toBe('TITLE');
expect(selectToasterContent('.toast-message')).toBe('MESSAGE');
});
test.skip('should display a warning toast', () => {
service.warn('test', 'title');
spectator.detectChanges();
expect(spectator.query('.toast-icon i')).toHaveClass('fa-exclamation-triangle');
test.each`
type | selector | icon
${'info'} | ${'.toast-info'} | ${'.fa-info-circle'}
${'success'} | ${'.toast-success'} | ${'.fa-check-circle'}
${'warn'} | ${'.toast-warning'} | ${'.fa-exclamation-triangle'}
${'error'} | ${'.toast-error'} | ${'.fa-times-circle'}
`('should display $type toast', async ({ type, selector, icon }) => {
service[type]('MESSAGE', 'TITLE');
await timer(0).toPromise();
service['containerComponentRef'].changeDetectorRef.detectChanges();
expect(selectToasterContent('.toast-title')).toBe('TITLE');
expect(selectToasterContent('.toast-message')).toBe('MESSAGE');
expect(selectToasterElement()).toBe(document.querySelector(selector));
expect(selectToasterElement(icon)).toBeTruthy();
});
test.skip('should display a success toast', () => {
service.success('test', 'title');
spectator.detectChanges();
expect(spectator.query('.toast-icon i')).toHaveClass('fa-check-circle');
test('should display multiple toasts', async () => {
service.show('MESSAGE_1', 'TITLE_1');
service.show('MESSAGE_2', 'TITLE_2');
await timer(0).toPromise();
service['containerComponentRef'].changeDetectorRef.detectChanges();
const titles = document.querySelectorAll('.toast-title');
expect(titles.length).toBe(2);
const messages = document.querySelectorAll('.toast-message');
expect(messages.length).toBe(2);
});
test.skip('should display an info toast', () => {
service.info('test', 'title');
spectator.detectChanges();
expect(spectator.query('.toast-icon i')).toHaveClass('fa-info-circle');
test('should remove a toast when remove is called', async () => {
service.show('MESSAGE');
service.remove(0);
await timer(0).toPromise();
service['containerComponentRef'].changeDetectorRef.detectChanges();
expect(selectToasterElement()).toBeNull();
});
test.skip('should display multiple toasts', () => {
service.info('detail1', 'summary1');
service.info('detail2', 'summary2');
spectator.detectChanges();
expect(spectator.queryAll('div.toast-title').map(node => node.textContent.trim())).toEqual([
'summary1',
'summary2',
]);
expect(spectator.queryAll('p.toast-message').map(node => node.textContent.trim())).toEqual([
'detail1',
'detail2',
]);
test('should remove toasts when clear is called', async () => {
service.show('MESSAGE');
service.clear();
await timer(0).toPromise();
service['containerComponentRef'].changeDetectorRef.detectChanges();
expect(selectToasterElement()).toBeNull();
});
test.skip('should remove the opened toasts', () => {
service.info('test', 'title');
spectator.detectChanges();
expect(spectator.query('div.toast')).toBeTruthy();
test('should remove toasts based on containerKey when clear is called with key', async () => {
service.show('MESSAGE_1', 'TITLE_1', 'neutral', { containerKey: 'x' });
service.show('MESSAGE_2', 'TITLE_2', 'neutral', { containerKey: 'y' });
service.clear('x');
service.clear();
spectator.detectChanges();
expect(spectator.query('p-div.toast')).toBeFalsy();
await timer(0).toPromise();
service['containerComponentRef'].changeDetectorRef.detectChanges();
expect(selectToasterElement('.fa-exclamation-circle')).toBeTruthy();
expect(selectToasterContent('.toast-title')).toBe('TITLE_2');
expect(selectToasterContent('.toast-message')).toBe('MESSAGE_2');
});
});
function clearElements(selector = '.toast') {
document.querySelectorAll(selector).forEach(element => element.parentNode.removeChild(element));
}
function selectToasterContent(selector = '.toast'): string {
return selectToasterElement(selector).textContent.trim();
}
function selectToasterElement<T extends HTMLElement>(selector = '.toast'): T {
return document.querySelector(selector);
}

Loading…
Cancel
Save