mirror of https://github.com/abpframework/abp.git
34 changed files with 1102 additions and 429 deletions
@ -1,3 +1,179 @@ |
|||||
## HTTP请求 |
## 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 |
# 如何懒加载 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 { CoreModule } from '@abp/ng.core'; |
||||
import { Component } from '@angular/core'; |
import { NgModule } from '@angular/core'; |
||||
import { RouterTestingModule } from '@angular/router/testing'; |
import { fakeAsync, tick } from '@angular/core/testing'; |
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; |
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; |
||||
import { NgxsModule } from '@ngxs/store'; |
import { NgxsModule } from '@ngxs/store'; |
||||
import { ConfirmationService } from '../services/confirmation.service'; |
import { timer } from 'rxjs'; |
||||
import { ThemeSharedModule } from '../theme-shared.module'; |
import { take } from 'rxjs/operators'; |
||||
import { OAuthModule, OAuthService } from 'angular-oauth2-oidc'; |
import { ConfirmationComponent } from '../components'; |
||||
|
import { Confirmation } from '../models'; |
||||
@Component({ |
import { ConfirmationService } from '../services'; |
||||
selector: 'abp-dummy', |
|
||||
template: ` |
@NgModule({ |
||||
<abp-confirmation></abp-confirmation> |
exports: [ConfirmationComponent], |
||||
`,
|
entryComponents: [ConfirmationComponent], |
||||
|
declarations: [ConfirmationComponent], |
||||
|
imports: [CoreModule.forTest()], |
||||
}) |
}) |
||||
class DummyComponent { |
export class MockModule {} |
||||
constructor(public confirmation: ConfirmationService) {} |
|
||||
} |
|
||||
|
|
||||
describe('ConfirmationService', () => { |
describe('ConfirmationService', () => { |
||||
let spectator: Spectator<DummyComponent>; |
let spectator: SpectatorService<ConfirmationService>; |
||||
let service: ConfirmationService; |
let service: ConfirmationService; |
||||
const createComponent = createComponentFactory({ |
const createService = createServiceFactory({ |
||||
component: DummyComponent, |
service: ConfirmationService, |
||||
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot(), RouterTestingModule], |
imports: [NgxsModule.forRoot(), CoreModule.forTest(), MockModule], |
||||
mocks: [OAuthService], |
|
||||
}); |
}); |
||||
|
|
||||
beforeEach(() => { |
beforeEach(() => { |
||||
spectator = createComponent(); |
spectator = createService(); |
||||
service = spectator.get(ConfirmationService); |
service = spectator.service; |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
clearElements(); |
||||
}); |
}); |
||||
|
|
||||
test.skip('should display a confirmation popup', () => { |
test('should display a confirmation popup', fakeAsync(() => { |
||||
service.info('test', 'title'); |
service.show('MESSAGE', 'TITLE'); |
||||
|
|
||||
spectator.detectChanges(); |
tick(); |
||||
|
|
||||
expect(spectator.query('div.confirmation .title')).toHaveText('title'); |
expect(selectConfirmationContent('.title')).toBe('TITLE'); |
||||
expect(spectator.query('div.confirmation .message')).toHaveText('test'); |
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 => { |
test('should close with ESC key', done => { |
||||
service.info('test', 'title').subscribe(() => { |
service |
||||
setTimeout(() => { |
.info('', '') |
||||
spectator.detectComponentChanges(); |
.pipe(take(1)) |
||||
expect(spectator.query('div.confirmation')).toBeFalsy(); |
.subscribe(status => { |
||||
|
expect(status).toBe(Confirmation.Status.dismiss); |
||||
done(); |
done(); |
||||
}, 0); |
}); |
||||
}); |
|
||||
|
|
||||
spectator.detectChanges(); |
const escape = new KeyboardEvent('keyup', { key: 'Escape' }); |
||||
expect(spectator.query('div.confirmation')).toBeTruthy(); |
document.dispatchEvent(escape); |
||||
spectator.dispatchKeyboardEvent('div.confirmation', 'keyup', 'Escape'); |
|
||||
}); |
}); |
||||
|
|
||||
test.skip('should close when click cancel button', done => { |
test('should close when click cancel button', async done => { |
||||
service.info('test', 'title', { yesText: 'Sure', cancelText: 'Exit' }).subscribe(() => { |
service.info('', '', { yesText: 'Sure', cancelText: 'Exit' }).subscribe(status => { |
||||
spectator.detectComponentChanges(); |
expect(status).toBe(Confirmation.Status.reject); |
||||
setTimeout(() => { |
done(); |
||||
expect(spectator.query('div.confirmation')).toBeFalsy(); |
|
||||
done(); |
|
||||
}, 0); |
|
||||
}); |
}); |
||||
|
|
||||
spectator.detectChanges(); |
await timer(0).toPromise(); |
||||
|
|
||||
expect(spectator.query('div.confirmation')).toBeTruthy(); |
expect(selectConfirmationContent('button#cancel')).toBe('Exit'); |
||||
expect(spectator.query('button#cancel')).toHaveText('Exit'); |
expect(selectConfirmationContent('button#confirm')).toBe('Sure'); |
||||
expect(spectator.query('button#confirm')).toHaveText('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 { CoreModule } from '@abp/ng.core'; |
||||
import { Component } from '@angular/core'; |
import { NgModule } from '@angular/core'; |
||||
import { RouterTestingModule } from '@angular/router/testing'; |
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; |
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; |
|
||||
import { NgxsModule } from '@ngxs/store'; |
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 { ToasterService } from '../services/toaster.service'; |
||||
import { ThemeSharedModule } from '../theme-shared.module'; |
|
||||
import { OAuthService } from 'angular-oauth2-oidc'; |
@NgModule({ |
||||
|
exports: [ToastContainerComponent], |
||||
@Component({ |
entryComponents: [ToastContainerComponent], |
||||
selector: 'abp-dummy', |
declarations: [ToastContainerComponent, ToastComponent], |
||||
template: ` |
imports: [CoreModule.forTest()], |
||||
<abp-toast-container></abp-toast-container> |
|
||||
`,
|
|
||||
}) |
}) |
||||
class DummyComponent { |
export class MockModule {} |
||||
constructor(public toaster: ToasterService) {} |
|
||||
} |
|
||||
|
|
||||
describe('ToasterService', () => { |
describe('ToasterService', () => { |
||||
let spectator: Spectator<DummyComponent>; |
let spectator: SpectatorService<ToasterService>; |
||||
let service: ToasterService; |
let service: ToasterService; |
||||
const createComponent = createComponentFactory({ |
const createService = createServiceFactory({ |
||||
component: DummyComponent, |
service: ToasterService, |
||||
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot(), RouterTestingModule], |
imports: [NgxsModule.forRoot(), CoreModule.forTest(), MockModule], |
||||
mocks: [OAuthService], |
|
||||
}); |
}); |
||||
|
|
||||
beforeEach(() => { |
beforeEach(() => { |
||||
spectator = createComponent(); |
spectator = createService(); |
||||
service = spectator.get(ToasterService); |
service = spectator.service; |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
clearElements(); |
||||
}); |
}); |
||||
|
|
||||
test.skip('should display an error toast', () => { |
test('should display a toast', async () => { |
||||
service.error('test', 'title'); |
service.show('MESSAGE', 'TITLE'); |
||||
|
|
||||
spectator.detectChanges(); |
await timer(0).toPromise(); |
||||
|
service['containerComponentRef'].changeDetectorRef.detectChanges(); |
||||
|
|
||||
expect(spectator.query('div.toast')).toBeTruthy(); |
expect(selectToasterElement('.fa-exclamation-circle')).toBeTruthy(); |
||||
expect(spectator.query('.toast-icon i')).toHaveClass('fa-times-circle'); |
expect(selectToasterContent('.toast-title')).toBe('TITLE'); |
||||
expect(spectator.query('div.toast-title')).toHaveText('title'); |
expect(selectToasterContent('.toast-message')).toBe('MESSAGE'); |
||||
expect(spectator.query('p.toast-message')).toHaveText('test'); |
|
||||
}); |
}); |
||||
|
|
||||
test.skip('should display a warning toast', () => { |
test.each` |
||||
service.warn('test', 'title'); |
type | selector | icon |
||||
spectator.detectChanges(); |
${'info'} | ${'.toast-info'} | ${'.fa-info-circle'} |
||||
expect(spectator.query('.toast-icon i')).toHaveClass('fa-exclamation-triangle'); |
${'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', () => { |
test('should display multiple toasts', async () => { |
||||
service.success('test', 'title'); |
service.show('MESSAGE_1', 'TITLE_1'); |
||||
spectator.detectChanges(); |
service.show('MESSAGE_2', 'TITLE_2'); |
||||
expect(spectator.query('.toast-icon i')).toHaveClass('fa-check-circle'); |
|
||||
|
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', () => { |
test('should remove a toast when remove is called', async () => { |
||||
service.info('test', 'title'); |
service.show('MESSAGE'); |
||||
spectator.detectChanges(); |
service.remove(0); |
||||
expect(spectator.query('.toast-icon i')).toHaveClass('fa-info-circle'); |
|
||||
|
await timer(0).toPromise(); |
||||
|
service['containerComponentRef'].changeDetectorRef.detectChanges(); |
||||
|
|
||||
|
expect(selectToasterElement()).toBeNull(); |
||||
}); |
}); |
||||
|
|
||||
test.skip('should display multiple toasts', () => { |
test('should remove toasts when clear is called', async () => { |
||||
service.info('detail1', 'summary1'); |
service.show('MESSAGE'); |
||||
service.info('detail2', 'summary2'); |
service.clear(); |
||||
|
|
||||
spectator.detectChanges(); |
await timer(0).toPromise(); |
||||
expect(spectator.queryAll('div.toast-title').map(node => node.textContent.trim())).toEqual([ |
service['containerComponentRef'].changeDetectorRef.detectChanges(); |
||||
'summary1', |
|
||||
'summary2', |
expect(selectToasterElement()).toBeNull(); |
||||
]); |
|
||||
expect(spectator.queryAll('p.toast-message').map(node => node.textContent.trim())).toEqual([ |
|
||||
'detail1', |
|
||||
'detail2', |
|
||||
]); |
|
||||
}); |
}); |
||||
|
|
||||
test.skip('should remove the opened toasts', () => { |
test('should remove toasts based on containerKey when clear is called with key', async () => { |
||||
service.info('test', 'title'); |
service.show('MESSAGE_1', 'TITLE_1', 'neutral', { containerKey: 'x' }); |
||||
spectator.detectChanges(); |
service.show('MESSAGE_2', 'TITLE_2', 'neutral', { containerKey: 'y' }); |
||||
expect(spectator.query('div.toast')).toBeTruthy(); |
service.clear('x'); |
||||
|
|
||||
service.clear(); |
await timer(0).toPromise(); |
||||
spectator.detectChanges(); |
service['containerComponentRef'].changeDetectorRef.detectChanges(); |
||||
expect(spectator.query('p-div.toast')).toBeFalsy(); |
|
||||
|
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 { enableProdMode } from '@angular/core'; |
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; |
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; |
||||
import { BootstrapModuleFn as Bootstrap, hmr, WebpackModule } from '@ngxs/hmr-plugin'; |
|
||||
|
|
||||
import { AppModule } from './app/app.module'; |
import { AppModule } from './app/app.module'; |
||||
import { environment } from './environments/environment'; |
import { environment } from './environments/environment'; |
||||
|
|
||||
declare const module: WebpackModule; |
|
||||
|
|
||||
if (environment.production) { |
if (environment.production) { |
||||
enableProdMode(); |
enableProdMode(); |
||||
} |
} |
||||
|
|
||||
const bootstrap: Bootstrap = () => platformBrowserDynamic().bootstrapModule(AppModule); |
platformBrowserDynamic() |
||||
|
.bootstrapModule(AppModule) |
||||
if (environment.hmr) { |
.catch((err) => console.error(err)); |
||||
hmr(module, bootstrap).catch(err => console.error(err)); |
|
||||
} else { |
|
||||
bootstrap().catch(err => console.log(err)); |
|
||||
} |
|
||||
|
|||||
Loading…
Reference in new issue