mirror of https://github.com/abpframework/abp.git
34 changed files with 1102 additions and 429 deletions
@ -1,3 +1,179 @@ |
|||
## HTTP请求 |
|||
|
|||
TODO... |
|||
## 关于 HttpClient |
|||
|
|||
Angular具有很棒的 `HttpClient` 与后端服务进行通信. 它位于顶层,是[XMLHttpRequest Web API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)的封装. 同时也是Angular建议用于任何HTTP请求的代理,在你的ABP项目中使用 `HttpClient` 是最佳做法. |
|||
|
|||
但是 `HttpClient` 将错误处理留给调用方,换句话说HTTP错误是通过手动处理的,通过挂接到返回的 `Observable` 的观察者中来处理. |
|||
|
|||
```js |
|||
getConfig() { |
|||
this.http.get(this.configUrl).subscribe( |
|||
config => this.updateConfig(config), |
|||
error => { |
|||
// Handle error here |
|||
}, |
|||
); |
|||
} |
|||
``` |
|||
|
|||
上面的代码尽管清晰灵活,但即使将错误处理委派给Store或任何其他注入. 以这种方式处理错误也是重复性的工作. |
|||
|
|||
`HttpInterceptor` 能够捕获 `HttpErrorResponse` 并可用于集中的错误处理. 然而,在必须放置错误处理程序(也就是拦截器)的情况下,需要额外的工作以及对Angular内部机制的理解. 检查[这个issue](https://github.com/angular/angular/issues/20203)了解详情. |
|||
|
|||
## RestService |
|||
|
|||
ABP核心模块有用于HTTP请求的实用程序服务: `RestService`. 除非另有明确配置,否则它将捕获HTTP错误并调度 `RestOccurError` 操作, 然后由 `ThemeSharedModule` 引入的 `ErrorHandler` 捕获此操作. 你应该已经在应用程序中导入了此模块,在使用 `RestService` 时,默认情况下将自动处理所有HTTP错误. |
|||
|
|||
### RestService 入门 |
|||
|
|||
为了使用 `RestService`, 你必须将它注入到你的类中. |
|||
|
|||
```js |
|||
import { RestService } from '@abp/ng.core'; |
|||
|
|||
@Injectable({ |
|||
/* class metadata here */ |
|||
}) |
|||
class DemoService { |
|||
constructor(private rest: RestService) {} |
|||
} |
|||
``` |
|||
|
|||
你不必在模块或组件/指令级别提供 `estService`,因为它已经在**根中**中提供了. |
|||
|
|||
### 如何使用RestService发出请求 |
|||
|
|||
你可以使用 `RestService` 的 `request` 方法来处理HTTP请求. 示例: |
|||
|
|||
```js |
|||
getFoo(id: number) { |
|||
const request: Rest.Request<null> = { |
|||
method: 'GET', |
|||
url: '/api/some/path/to/foo/' + id, |
|||
}; |
|||
|
|||
return this.rest.request<null, FooResponse>(request); |
|||
} |
|||
``` |
|||
|
|||
`request` 方法始终返回 `Observable<T>`. 无论何时使用 `getFoo` 方法,都可以执行以下操作: |
|||
|
|||
```js |
|||
doSomethingWithFoo(id: number) { |
|||
this.demoService.getFoo(id).subscribe( |
|||
foo => { |
|||
// Do something with foo. |
|||
} |
|||
) |
|||
} |
|||
``` |
|||
|
|||
**你不必担心关于取消订阅**. `RestService` 在内部使用 `HttpClient`,因此它返回的每个可观察对象都是有限的可观察对象,成功或出错后将自动关闭订阅. |
|||
|
|||
如你所见,`request` 方法获取一个具有 `Rest.Reques<T>` 类型的请求选项对象. 此泛型类型需要请求主体的接口. 当没有正文时,例如在 `GET` 或 `DELETE` 请求中,你可以传递 `null`. 示例: |
|||
|
|||
```js |
|||
postFoo(body: Foo) { |
|||
const request: Rest.Request<Foo> = { |
|||
method: 'POST', |
|||
url: '/api/some/path/to/foo', |
|||
body |
|||
}; |
|||
|
|||
return this.rest.request<Foo, FooResponse>(request); |
|||
} |
|||
``` |
|||
|
|||
你可以在[此处检查](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23)完整的 `Rest.Request<T>` 类型,与Angular中的[HttpRequest](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23)类相比只有很少的改动. |
|||
|
|||
### 如何禁用RestService的默认错误处理程序 |
|||
|
|||
默认 `request` 方法始终处理错误. 让我们看看如何改变这种行为并由自己处理错误: |
|||
|
|||
```js |
|||
deleteFoo(id: number) { |
|||
const request: Rest.Request<null> = { |
|||
method: 'DELETE', |
|||
url: '/api/some/path/to/foo/' + id, |
|||
}; |
|||
|
|||
return this.rest.request<null, void>(request, { skipHandleError: true }); |
|||
} |
|||
``` |
|||
|
|||
`skipHandleError` 配置选项设置为 `true` 时,禁用错误处理程序,并返回 `observable` 引发错误,你可以在订阅中捕获该错误. |
|||
|
|||
```js |
|||
removeFooFromList(id: number) { |
|||
this.demoService.deleteFoo(id).subscribe( |
|||
foo => { |
|||
// Do something with foo. |
|||
}, |
|||
error => { |
|||
// Do something with error. |
|||
} |
|||
) |
|||
} |
|||
``` |
|||
|
|||
### 如何从应用程序配置获取特定的API端点 |
|||
|
|||
`request` 方法接收到的另一个配置选项是 `apiName` (在v2.4中可用),它用于从应用程序配置获取特定的模块端点. |
|||
|
|||
```js |
|||
putFoo(body: Foo, id: string) { |
|||
const request: Rest.Request<Foo> = { |
|||
method: 'PUT', |
|||
url: '/' + id, |
|||
body |
|||
}; |
|||
|
|||
return this.rest.request<Foo, void>(request, {apiName: 'foo'}); |
|||
} |
|||
``` |
|||
|
|||
上面的putFoo将请求 `https://localhost:44305/api/some/path/to/foo/{id}` 当环境变量如下: |
|||
|
|||
```js |
|||
// environment.ts |
|||
|
|||
export const environment = { |
|||
apis: { |
|||
default: { |
|||
url: 'https://localhost:44305', |
|||
}, |
|||
foo: { |
|||
url: 'https://localhost:44305/api/some/path/to/foo', |
|||
}, |
|||
}, |
|||
|
|||
/* rest of the environment variables here */ |
|||
} |
|||
``` |
|||
|
|||
### 如何观察响应对象或HTTP事件而不是正文 |
|||
|
|||
`RestService` 假定你通常对响应的正文感兴趣,默认情况下将 `observe` 属性设置为 `body`. 但是有时你可能对其他内容(例如自定义标头)非常感兴趣. 为此, `request` 方法在 `config` 对象中接收 `watch` 属性. |
|||
|
|||
```js |
|||
getSomeCustomHeaderValue() { |
|||
const request: Rest.Request<null> = { |
|||
method: 'GET', |
|||
url: '/api/some/path/that/sends/some-custom-header', |
|||
}; |
|||
|
|||
return this.rest.request<null, HttpResponse<any>>( |
|||
request, |
|||
{observe: Rest.Observe.Response}, |
|||
).pipe( |
|||
map(response => response.headers.get('Some-Custom-Header')) |
|||
); |
|||
} |
|||
``` |
|||
|
|||
你可以在[此处](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10)找到 `Rest.Observe` 枚举. |
|||
|
|||
## 下一步是什么? |
|||
|
|||
* [本地化](./Localization.md) |
|||
@ -1,3 +1,189 @@ |
|||
# 如何懒加载 Scripts 与 Styles |
|||
|
|||
TODO... |
|||
你可以使用@abp/ng.core包中的 `LazyLoadService` 以简单明了的方式延迟加载脚本和样式. |
|||
|
|||
## 入门 |
|||
|
|||
你不必在模块或组件/指令级别提供 `LazyLoadService`,因为它已经在**根中**中提供了. 你可以在组件,指令或服务中注入并使用它. |
|||
|
|||
```js |
|||
import { LazyLoadService } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
/* class metadata here */ |
|||
}) |
|||
class DemoComponent { |
|||
constructor(private lazyLoadService: LazyLoadService) {} |
|||
} |
|||
``` |
|||
|
|||
## 用法 |
|||
|
|||
你可以使用 `LazyLoadService` 的 `load` 方法在DOM中的所需位置创建 `<script>` 或 `<link>` 元素并强制浏览器下载目标资源. |
|||
|
|||
### 如何加载 Scripts |
|||
|
|||
`load` 方法的第一个参数需要一个 `LoadingStrategy`. 如果传递 `ScriptLoadingStrategy` 实例,`LazyLoadService` 将使用给定的 `src` 创建一个 `<script>` 元素并放置在指定的DOM位置. |
|||
|
|||
```js |
|||
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
template: ` |
|||
<some-component *ngIf="libraryLoaded$ | async"></some-component> |
|||
` |
|||
}) |
|||
class DemoComponent { |
|||
libraryLoaded$ = this.lazyLoad.load( |
|||
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/some-library.js'), |
|||
); |
|||
|
|||
constructor(private lazyLoadService: LazyLoadService) {} |
|||
} |
|||
``` |
|||
|
|||
`load` 方法返回一个 `observable`,你可以在组件中或通过 `AsyncPipe` 订阅它. 在上面的示例中**仅当脚本成功加载或之前已经加载脚本时**, `NgIf` 指令才会呈现 `<some-component>`. |
|||
|
|||
> 你可以使用 `async` 管道在模板中多次订阅,脚本将仅加载一次 |
|||
|
|||
请参阅[LoadingStrategy](./Loading-Strategy.md)查看所有可用的加载策略以及如何构建自己的加载策略. |
|||
|
|||
### 如何加载 Styles |
|||
|
|||
如果传递给 `load` 方法第一个参数为 `StyleLoadingStrategy` 实例,`LazyLoadService` 将使用给定的 `href` 创建一个 `<link>` 元素并放置在指定的DOM位置. |
|||
|
|||
```js |
|||
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
template: ` |
|||
<some-component *ngIf="stylesLoaded$ | async"></some-component> |
|||
` |
|||
}) |
|||
class DemoComponent { |
|||
stylesLoaded$ = this.lazyLoad.load( |
|||
LOADING_STRATEGY.AppendAnonymousStyleToHead('/assets/some-styles.css'), |
|||
); |
|||
|
|||
constructor(private lazyLoadService: LazyLoadService) {} |
|||
} |
|||
``` |
|||
|
|||
`load` 方法返回一个 `observable`,你可以在组件中或通过 `AsyncPipe` 订阅它. 在上面的示例中**仅当样式成功加载或之前已经加载样式时**, `NgIf` 指令才会呈现 `<some-component>`. |
|||
|
|||
> 你可以使用 `async` 管道在模板中多次订阅,样式将仅加载一次 |
|||
|
|||
请参阅[LoadingStrategy](./Loading-Strategy.md)查看所有可用的加载策略以及如何构建自己的加载策略. |
|||
|
|||
### 高级用法 |
|||
|
|||
你有**很大的自由度来定义延迟加载的工作方式**. 示例: |
|||
|
|||
```js |
|||
const domStrategy = DOM_STRATEGY.PrependToHead(); |
|||
|
|||
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous( |
|||
'sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh', |
|||
); |
|||
|
|||
const loadingStrategy = new StyleLoadingStrategy( |
|||
'https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css', |
|||
domStrategy, |
|||
crossOriginStrategy, |
|||
); |
|||
|
|||
this.lazyLoad.load(loadingStrategy, 1, 2000); |
|||
``` |
|||
|
|||
此代码将创建具有给定URL和完整性哈希的 `<link>` 元素,将其插入到 `<head>` 元素的顶部,如果第一次尝试失败,则在2秒后重试一次. |
|||
|
|||
一个常见的用例是在**使用功能之前加载多个脚本/样式**: |
|||
|
|||
```js |
|||
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
|||
import { frokJoin } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
template: ` |
|||
<some-component *ngIf="scriptsAndStylesLoaded$ | async"></some-component> |
|||
` |
|||
}) |
|||
class DemoComponent { |
|||
private stylesLoaded$ = forkJoin( |
|||
this.lazyLoad.load( |
|||
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library-dark-theme.css'), |
|||
), |
|||
this.lazyLoad.load( |
|||
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library.css'), |
|||
), |
|||
); |
|||
|
|||
private scriptsLoaded$ = forkJoin( |
|||
this.lazyLoad.load( |
|||
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/library.js'), |
|||
), |
|||
this.lazyLoad.load( |
|||
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/other-library.css'), |
|||
), |
|||
); |
|||
|
|||
scriptsAndStylesLoaded$ = forkJoin(this.scriptsLoaded$, this.stylesLoaded$); |
|||
|
|||
constructor(private lazyLoadService: LazyLoadService) {} |
|||
} |
|||
``` |
|||
|
|||
RxJS `forkJoin` 并行加载所有脚本和样式,并仅在加载所有脚本和样式时才放行. 因此放置 `<some-component>` 时,所有必需的依赖项都可用的. |
|||
|
|||
> 注意到我们在文档头上添加了样式吗? 有时这是必需的因为你的应用程序样式可能会覆盖某些库样式. 在这种情况下你必须注意前置样式的顺序. 它们将一一放置,**并且在放置前,最后一个放置在最上面**. |
|||
|
|||
另一个常见的用例是**按顺序加载依赖脚本**: |
|||
|
|||
```js |
|||
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
|||
import { concat } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
template: ` |
|||
<some-component *ngIf="scriptsLoaded$ | async"></some-component> |
|||
` |
|||
}) |
|||
class DemoComponent { |
|||
scriptsLoaded$ = concat( |
|||
this.lazyLoad.load( |
|||
LOADING_STRATEGY.PrependAnonymousScriptToHead('/assets/library.js'), |
|||
), |
|||
this.lazyLoad.load( |
|||
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/script-that-requires-library.js'), |
|||
), |
|||
); |
|||
|
|||
constructor(private lazyLoadService: LazyLoadService) {} |
|||
} |
|||
``` |
|||
|
|||
在此示例中,第二个文件需要预先加载第一个文件, RxJS `concat` 函数将允许你以给定的顺序一个接一个地加载所有脚本,并且仅在加载所有脚本时放行. |
|||
|
|||
## API |
|||
|
|||
### loaded |
|||
|
|||
```js |
|||
loaded: Set<string> |
|||
``` |
|||
|
|||
所有以前加载的路径都可以通过此属性访问. 它是一个简单的[JavaScript集] |
|||
|
|||
### load |
|||
|
|||
```js |
|||
load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable<Event> |
|||
``` |
|||
|
|||
- `strategy` 是主要参数,上面已经介绍过. |
|||
- `retryTimes` 定义加载失败前再次尝试多少次(默认值:2). |
|||
- `retryDelay` 定义重试之间的延迟(默认为1000). |
|||
|
|||
## 下一步是什么? |
|||
|
|||
- [DomInsertionService](./Dom-Insertion-Service.md) |
|||
@ -0,0 +1,8 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { LocalizationPipe } from './pipes/localization.pipe'; |
|||
|
|||
@NgModule({ |
|||
exports: [LocalizationPipe], |
|||
declarations: [LocalizationPipe], |
|||
}) |
|||
export class LocalizationModule {} |
|||
@ -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); |
|||
} |
|||
|
|||
@ -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); |
|||
} |
|||
|
|||
@ -0,0 +1,2 @@ |
|||
export * from './append-content.token'; |
|||
export * from './http-error.token'; |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"extends": "./tsconfig.lib.json", |
|||
"angularCompilerOptions": { |
|||
"enableIvy": false |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"extends": "./tsconfig.lib.json", |
|||
"angularCompilerOptions": { |
|||
"enableIvy": false |
|||
} |
|||
} |
|||
@ -1,25 +0,0 @@ |
|||
export const environment = { |
|||
production: false, |
|||
hmr: true, |
|||
application: { |
|||
name: 'MyProjectName', |
|||
logoUrl: '', |
|||
}, |
|||
oAuthConfig: { |
|||
issuer: 'https://localhost:44301', |
|||
clientId: 'MyProjectName_ConsoleTestApp', |
|||
dummyClientSecret: '1q2w3e*', |
|||
scope: 'MyProjectName', |
|||
showDebugInformation: true, |
|||
oidc: false, |
|||
requireHttps: true, |
|||
}, |
|||
apis: { |
|||
default: { |
|||
url: 'https://localhost:44300', |
|||
}, |
|||
}, |
|||
localization: { |
|||
defaultResourceName: 'MyProjectName', |
|||
}, |
|||
}; |
|||
@ -1,20 +1,13 @@ |
|||
import { enableProdMode } from '@angular/core'; |
|||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; |
|||
import { BootstrapModuleFn as Bootstrap, hmr, WebpackModule } from '@ngxs/hmr-plugin'; |
|||
|
|||
import { AppModule } from './app/app.module'; |
|||
import { environment } from './environments/environment'; |
|||
|
|||
declare const module: WebpackModule; |
|||
|
|||
if (environment.production) { |
|||
enableProdMode(); |
|||
} |
|||
|
|||
const bootstrap: Bootstrap = () => platformBrowserDynamic().bootstrapModule(AppModule); |
|||
|
|||
if (environment.hmr) { |
|||
hmr(module, bootstrap).catch(err => console.error(err)); |
|||
} else { |
|||
bootstrap().catch(err => console.log(err)); |
|||
} |
|||
platformBrowserDynamic() |
|||
.bootstrapModule(AppModule) |
|||
.catch((err) => console.error(err)); |
|||
|
|||
Loading…
Reference in new issue