mirror of https://github.com/abpframework/abp
commit
48d030c1da
@ -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