mirror of https://github.com/abpframework/abp.git
39 changed files with 420 additions and 750 deletions
@ -1,6 +1,6 @@ |
|||||
using System; |
using System; |
||||
|
|
||||
namespace Volo.Abp.Http.Client.Proxying |
namespace Volo.Abp.Http.Client.ClientProxying |
||||
{ |
{ |
||||
public class ApiVersionInfo //TODO: Rename to not conflict with api versioning apis
|
public class ApiVersionInfo //TODO: Rename to not conflict with api versioning apis
|
||||
{ |
{ |
||||
@ -0,0 +1,19 @@ |
|||||
|
using System.Net.Http; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Http.Client.ClientProxying; |
||||
|
|
||||
|
namespace Volo.Abp.Http.Client.DynamicProxying |
||||
|
{ |
||||
|
public class DynamicHttpProxyInterceptorClientProxy<TService> : ClientProxyBase<TService> |
||||
|
{ |
||||
|
public virtual async Task<T> CallRequestAsync<T>(ClientProxyRequestContext requestContext) |
||||
|
{ |
||||
|
return await base.RequestAsync<T>(requestContext); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<HttpContent> CallRequestAsync(ClientProxyRequestContext requestContext) |
||||
|
{ |
||||
|
return await base.RequestAsync(requestContext); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,267 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Globalization; |
|
||||
using System.Linq; |
|
||||
using System.Net.Http; |
|
||||
using System.Net.Http.Headers; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using Microsoft.Extensions.Options; |
|
||||
using Microsoft.Extensions.Primitives; |
|
||||
using Volo.Abp.Content; |
|
||||
using Volo.Abp.DependencyInjection; |
|
||||
using Volo.Abp.Http.Client.Authentication; |
|
||||
using Volo.Abp.Http.Modeling; |
|
||||
using Volo.Abp.Http.ProxyScripting.Generators; |
|
||||
using Volo.Abp.Json; |
|
||||
using Volo.Abp.MultiTenancy; |
|
||||
using Volo.Abp.Threading; |
|
||||
using Volo.Abp.Tracing; |
|
||||
|
|
||||
namespace Volo.Abp.Http.Client.Proxying |
|
||||
{ |
|
||||
public class HttpProxyExecuter : IHttpProxyExecuter, ITransientDependency |
|
||||
{ |
|
||||
protected ICancellationTokenProvider CancellationTokenProvider { get; } |
|
||||
protected ICorrelationIdProvider CorrelationIdProvider { get; } |
|
||||
protected ICurrentTenant CurrentTenant { get; } |
|
||||
protected AbpCorrelationIdOptions AbpCorrelationIdOptions { get; } |
|
||||
protected IProxyHttpClientFactory HttpClientFactory { get; } |
|
||||
protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider { get; } |
|
||||
protected AbpHttpClientOptions ClientOptions { get; } |
|
||||
protected IJsonSerializer JsonSerializer { get; } |
|
||||
protected IRemoteServiceHttpClientAuthenticator ClientAuthenticator { get; } |
|
||||
|
|
||||
public HttpProxyExecuter( |
|
||||
ICancellationTokenProvider cancellationTokenProvider, |
|
||||
ICorrelationIdProvider correlationIdProvider, |
|
||||
ICurrentTenant currentTenant, |
|
||||
IOptions<AbpCorrelationIdOptions> abpCorrelationIdOptions, |
|
||||
IProxyHttpClientFactory httpClientFactory, |
|
||||
IRemoteServiceConfigurationProvider remoteServiceConfigurationProvider, |
|
||||
IOptions<AbpHttpClientOptions> clientOptions, |
|
||||
IRemoteServiceHttpClientAuthenticator clientAuthenticator, |
|
||||
IJsonSerializer jsonSerializer) |
|
||||
{ |
|
||||
CancellationTokenProvider = cancellationTokenProvider; |
|
||||
CorrelationIdProvider = correlationIdProvider; |
|
||||
CurrentTenant = currentTenant; |
|
||||
AbpCorrelationIdOptions = abpCorrelationIdOptions.Value; |
|
||||
HttpClientFactory = httpClientFactory; |
|
||||
RemoteServiceConfigurationProvider = remoteServiceConfigurationProvider; |
|
||||
ClientOptions = clientOptions.Value; |
|
||||
ClientAuthenticator = clientAuthenticator; |
|
||||
JsonSerializer = jsonSerializer; |
|
||||
} |
|
||||
|
|
||||
public virtual async Task<T> MakeRequestAndGetResultAsync<T>(HttpProxyExecuterContext context) |
|
||||
{ |
|
||||
var responseContent = await MakeRequestAsync(context); |
|
||||
|
|
||||
if (typeof(T) == typeof(IRemoteStreamContent) || |
|
||||
typeof(T) == typeof(RemoteStreamContent)) |
|
||||
{ |
|
||||
/* returning a class that holds a reference to response |
|
||||
* content just to be sure that GC does not dispose of |
|
||||
* it before we finish doing our work with the stream */ |
|
||||
return (T) (object) new RemoteStreamContent( |
|
||||
await responseContent.ReadAsStreamAsync(), |
|
||||
responseContent.Headers?.ContentDisposition?.FileNameStar ?? RemoveQuotes(responseContent.Headers?.ContentDisposition?.FileName).ToString(), |
|
||||
responseContent.Headers?.ContentType?.ToString(), |
|
||||
responseContent.Headers?.ContentLength); |
|
||||
} |
|
||||
|
|
||||
var stringContent = await responseContent.ReadAsStringAsync(); |
|
||||
if (typeof(T) == typeof(string)) |
|
||||
{ |
|
||||
return (T)(object)stringContent; |
|
||||
} |
|
||||
|
|
||||
if (stringContent.IsNullOrWhiteSpace()) |
|
||||
{ |
|
||||
return default; |
|
||||
} |
|
||||
|
|
||||
return JsonSerializer.Deserialize<T>(stringContent); |
|
||||
} |
|
||||
|
|
||||
public virtual async Task<HttpContent> MakeRequestAsync(HttpProxyExecuterContext context) |
|
||||
{ |
|
||||
var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(context.ServiceType) ?? throw new AbpException($"Could not get HttpClientProxyConfig for {context.ServiceType.FullName}."); |
|
||||
var remoteServiceConfig = await RemoteServiceConfigurationProvider.GetConfigurationOrDefaultAsync(clientConfig.RemoteServiceName); |
|
||||
|
|
||||
var client = HttpClientFactory.Create(clientConfig.RemoteServiceName); |
|
||||
|
|
||||
var apiVersion = await GetApiVersionInfoAsync(context); |
|
||||
var url = remoteServiceConfig.BaseUrl.EnsureEndsWith('/') + UrlBuilder.GenerateUrlWithParameters(context.Action, context.Arguments, apiVersion); |
|
||||
|
|
||||
var requestMessage = new HttpRequestMessage(context.Action.GetHttpMethod(), url) |
|
||||
{ |
|
||||
Content = RequestPayloadBuilder.BuildContent(context.Action, context.Arguments, JsonSerializer, apiVersion) |
|
||||
}; |
|
||||
|
|
||||
AddHeaders(context.Arguments, context.Action, requestMessage, apiVersion); |
|
||||
|
|
||||
if (context.Action.AllowAnonymous != true) |
|
||||
{ |
|
||||
await ClientAuthenticator.Authenticate( |
|
||||
new RemoteServiceHttpClientAuthenticateContext( |
|
||||
client, |
|
||||
requestMessage, |
|
||||
remoteServiceConfig, |
|
||||
clientConfig.RemoteServiceName |
|
||||
) |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
var response = await client.SendAsync( |
|
||||
requestMessage, |
|
||||
HttpCompletionOption.ResponseHeadersRead /*this will buffer only the headers, the content will be used as a stream*/, |
|
||||
GetCancellationToken(context.Arguments) |
|
||||
); |
|
||||
|
|
||||
if (!response.IsSuccessStatusCode) |
|
||||
{ |
|
||||
await ThrowExceptionForResponseAsync(response); |
|
||||
} |
|
||||
|
|
||||
return response.Content; |
|
||||
} |
|
||||
|
|
||||
private async Task<ApiVersionInfo> GetApiVersionInfoAsync(HttpProxyExecuterContext context) |
|
||||
{ |
|
||||
var apiVersion = await FindBestApiVersionAsync(context); |
|
||||
|
|
||||
//TODO: Make names configurable?
|
|
||||
var versionParam = context.Action.Parameters.FirstOrDefault(p => p.Name == "apiVersion" && p.BindingSourceId == ParameterBindingSources.Path) ?? |
|
||||
context.Action.Parameters.FirstOrDefault(p => p.Name == "api-version" && p.BindingSourceId == ParameterBindingSources.Query); |
|
||||
|
|
||||
return new ApiVersionInfo(versionParam?.BindingSourceId, apiVersion); |
|
||||
} |
|
||||
|
|
||||
private async Task<string> FindBestApiVersionAsync(HttpProxyExecuterContext context) |
|
||||
{ |
|
||||
var configuredVersion = await GetConfiguredApiVersionAsync(context); |
|
||||
|
|
||||
if (context.Action.SupportedVersions.IsNullOrEmpty()) |
|
||||
{ |
|
||||
return configuredVersion ?? "1.0"; |
|
||||
} |
|
||||
|
|
||||
if (context.Action.SupportedVersions.Contains(configuredVersion)) |
|
||||
{ |
|
||||
return configuredVersion; |
|
||||
} |
|
||||
|
|
||||
return context.Action.SupportedVersions.Last(); //TODO: Ensure to get the latest version!
|
|
||||
} |
|
||||
|
|
||||
private async Task<string> GetConfiguredApiVersionAsync(HttpProxyExecuterContext context) |
|
||||
{ |
|
||||
var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(context.ServiceType) |
|
||||
?? throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {context.ServiceType.FullName}."); |
|
||||
|
|
||||
return (await RemoteServiceConfigurationProvider |
|
||||
.GetConfigurationOrDefaultOrNullAsync(clientConfig.RemoteServiceName))?.Version; |
|
||||
} |
|
||||
|
|
||||
private async Task ThrowExceptionForResponseAsync(HttpResponseMessage response) |
|
||||
{ |
|
||||
if (response.Headers.Contains(AbpHttpConsts.AbpErrorFormat)) |
|
||||
{ |
|
||||
var errorResponse = JsonSerializer.Deserialize<RemoteServiceErrorResponse>( |
|
||||
await response.Content.ReadAsStringAsync() |
|
||||
); |
|
||||
|
|
||||
throw new AbpRemoteCallException(errorResponse.Error) |
|
||||
{ |
|
||||
HttpStatusCode = (int) response.StatusCode |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
throw new AbpRemoteCallException( |
|
||||
new RemoteServiceErrorInfo |
|
||||
{ |
|
||||
Message = response.ReasonPhrase, |
|
||||
Code = response.StatusCode.ToString() |
|
||||
} |
|
||||
) |
|
||||
{ |
|
||||
HttpStatusCode = (int) response.StatusCode |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
protected virtual void AddHeaders( |
|
||||
IReadOnlyDictionary<string, object> argumentsDictionary, |
|
||||
ActionApiDescriptionModel action, |
|
||||
HttpRequestMessage requestMessage, |
|
||||
ApiVersionInfo apiVersion) |
|
||||
{ |
|
||||
//API Version
|
|
||||
if (!apiVersion.Version.IsNullOrEmpty()) |
|
||||
{ |
|
||||
//TODO: What about other media types?
|
|
||||
requestMessage.Headers.Add("accept", $"{MimeTypes.Text.Plain}; v={apiVersion.Version}"); |
|
||||
requestMessage.Headers.Add("accept", $"{MimeTypes.Application.Json}; v={apiVersion.Version}"); |
|
||||
requestMessage.Headers.Add("api-version", apiVersion.Version); |
|
||||
} |
|
||||
|
|
||||
//Header parameters
|
|
||||
var headers = action.Parameters.Where(p => p.BindingSourceId == ParameterBindingSources.Header).ToArray(); |
|
||||
foreach (var headerParameter in headers) |
|
||||
{ |
|
||||
var value = HttpActionParameterHelper.FindParameterValue(argumentsDictionary, headerParameter); |
|
||||
if (value != null) |
|
||||
{ |
|
||||
requestMessage.Headers.Add(headerParameter.Name, value.ToString()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//CorrelationId
|
|
||||
requestMessage.Headers.Add(AbpCorrelationIdOptions.HttpHeaderName, CorrelationIdProvider.Get()); |
|
||||
|
|
||||
//TenantId
|
|
||||
if (CurrentTenant.Id.HasValue) |
|
||||
{ |
|
||||
//TODO: Use AbpAspNetCoreMultiTenancyOptions to get the key
|
|
||||
requestMessage.Headers.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString()); |
|
||||
} |
|
||||
|
|
||||
//Culture
|
|
||||
//TODO: Is that the way we want? Couldn't send the culture (not ui culture)
|
|
||||
var currentCulture = CultureInfo.CurrentUICulture.Name ?? CultureInfo.CurrentCulture.Name; |
|
||||
if (!currentCulture.IsNullOrEmpty()) |
|
||||
{ |
|
||||
requestMessage.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(currentCulture)); |
|
||||
} |
|
||||
|
|
||||
//X-Requested-With
|
|
||||
requestMessage.Headers.Add("X-Requested-With", "XMLHttpRequest"); |
|
||||
} |
|
||||
|
|
||||
protected virtual StringSegment RemoveQuotes(StringSegment input) |
|
||||
{ |
|
||||
if (!StringSegment.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"') |
|
||||
{ |
|
||||
input = input.Subsegment(1, input.Length - 2); |
|
||||
} |
|
||||
|
|
||||
return input; |
|
||||
} |
|
||||
|
|
||||
protected virtual CancellationToken GetCancellationToken(IReadOnlyDictionary<string, object> arguments) |
|
||||
{ |
|
||||
var cancellationTokenArg = arguments.LastOrDefault(); |
|
||||
|
|
||||
if (cancellationTokenArg.Value is CancellationToken cancellationToken) |
|
||||
{ |
|
||||
if (cancellationToken != default) |
|
||||
{ |
|
||||
return cancellationToken; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return CancellationTokenProvider.Token; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,12 +0,0 @@ |
|||||
using System.Net.Http; |
|
||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace Volo.Abp.Http.Client.Proxying |
|
||||
{ |
|
||||
public interface IHttpProxyExecuter |
|
||||
{ |
|
||||
Task<HttpContent> MakeRequestAsync(HttpProxyExecuterContext context); |
|
||||
|
|
||||
Task<T> MakeRequestAndGetResultAsync<T>(HttpProxyExecuterContext context); |
|
||||
} |
|
||||
} |
|
||||
@ -1,13 +0,0 @@ |
|||||
using System.Collections.Generic; |
|
||||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|
||||
|
|
||||
namespace Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup |
|
||||
{ |
|
||||
public class ProfileManagementScriptBundleContributor: BundleContributor |
|
||||
{ |
|
||||
public override void ConfigureBundle(BundleConfigurationContext context) |
|
||||
{ |
|
||||
context.Files.AddIfNotContains("/client-proxies/identity-proxy.js"); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; |
|
||||
import { of } from 'rxjs'; |
|
||||
import { ApplicationConfigurationService, RestService } from '../services'; |
|
||||
|
|
||||
describe('ApplicationConfigurationService', () => { |
|
||||
let spectator: SpectatorService<ApplicationConfigurationService>; |
|
||||
const createService = createServiceFactory({ |
|
||||
service: ApplicationConfigurationService, |
|
||||
mocks: [RestService], |
|
||||
}); |
|
||||
|
|
||||
beforeEach(() => (spectator = createService())); |
|
||||
|
|
||||
it('should send a GET to application-configuration API', () => { |
|
||||
const rest = spectator.inject(RestService); |
|
||||
|
|
||||
const requestSpy = jest.spyOn(rest, 'request'); |
|
||||
requestSpy.mockReturnValue(of(null)); |
|
||||
|
|
||||
spectator.service.getConfiguration().subscribe(); |
|
||||
|
|
||||
expect(requestSpy).toHaveBeenCalledWith( |
|
||||
{ method: 'GET', url: '/api/abp/application-configuration' }, |
|
||||
{}, |
|
||||
); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,126 +0,0 @@ |
|||||
import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator/jest'; |
|
||||
import { VisibilityDirective } from '../directives/visibility.directive'; |
|
||||
|
|
||||
describe('VisibilityDirective', () => { |
|
||||
let spectator: SpectatorDirective<VisibilityDirective>; |
|
||||
let directive: VisibilityDirective; |
|
||||
const createDirective = createDirectiveFactory({ |
|
||||
directive: VisibilityDirective, |
|
||||
}); |
|
||||
|
|
||||
describe('without content', () => { |
|
||||
beforeEach(() => { |
|
||||
spectator = createDirective('<div [abpVisibility]></div>'); |
|
||||
directive = spectator.directive; |
|
||||
}); |
|
||||
|
|
||||
it('should be created', () => { |
|
||||
expect(directive).toBeTruthy(); |
|
||||
}); |
|
||||
|
|
||||
xit('should be removed', done => { |
|
||||
setTimeout(() => { |
|
||||
expect(spectator.query('div')).toBeFalsy(); |
|
||||
done(); |
|
||||
}, 0); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
describe('without mutation observer and with content', () => { |
|
||||
beforeEach(() => { |
|
||||
spectator = createDirective('<div [abpVisibility]><p id="content">Content</p></div>'); |
|
||||
directive = spectator.directive; |
|
||||
}); |
|
||||
|
|
||||
it('should not removed', done => { |
|
||||
setTimeout(() => { |
|
||||
expect(spectator.query('div')).toBeTruthy(); |
|
||||
done(); |
|
||||
}, 0); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
describe('without mutation observer and with focused element', () => { |
|
||||
beforeEach(() => { |
|
||||
spectator = createDirective( |
|
||||
'<div id="main" [abpVisibility]="container"></div><div #container><p id="content">Content</p></div>', |
|
||||
); |
|
||||
directive = spectator.directive; |
|
||||
}); |
|
||||
|
|
||||
it('should not removed', done => { |
|
||||
setTimeout(() => { |
|
||||
expect(spectator.query('#main')).toBeTruthy(); |
|
||||
done(); |
|
||||
}, 0); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
describe('without content and with focused element', () => { |
|
||||
beforeEach(() => { |
|
||||
spectator = createDirective( |
|
||||
'<div id="main" [abpVisibility]="container"></div><div #container></div>', |
|
||||
); |
|
||||
directive = spectator.directive; |
|
||||
}); |
|
||||
|
|
||||
xit('should be removed', done => { |
|
||||
setTimeout(() => { |
|
||||
expect(spectator.query('#main')).toBeFalsy(); |
|
||||
done(); |
|
||||
}, 0); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
describe('with mutation observer and with content', () => { |
|
||||
beforeEach(() => { |
|
||||
spectator = createDirective('<div [abpVisibility]><div id="content">Content</div></div>'); |
|
||||
directive = spectator.directive; |
|
||||
}); |
|
||||
|
|
||||
xit('should remove the main div element when content removed', done => { |
|
||||
spectator.query('#content').remove(); |
|
||||
|
|
||||
setTimeout(() => { |
|
||||
expect(spectator.query('div')).toBeFalsy(); |
|
||||
done(); |
|
||||
}, 0); |
|
||||
}); |
|
||||
|
|
||||
it('should not remove the main div element', done => { |
|
||||
spectator.query('div').appendChild(document.createElement('div')); |
|
||||
|
|
||||
setTimeout(() => { |
|
||||
expect(spectator.query('div')).toBeTruthy(); |
|
||||
done(); |
|
||||
}, 100); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
describe('with mutation observer and with focused element', () => { |
|
||||
beforeEach(() => { |
|
||||
spectator = createDirective( |
|
||||
'<div id="main" [abpVisibility]="container"></div><div #container><p id="content">Content</p></div>', |
|
||||
); |
|
||||
directive = spectator.directive; |
|
||||
}); |
|
||||
|
|
||||
xit('should remove the main div element when content removed', done => { |
|
||||
spectator.query('#content').remove(); |
|
||||
|
|
||||
setTimeout(() => { |
|
||||
expect(spectator.query('#main')).toBeFalsy(); |
|
||||
done(); |
|
||||
}, 0); |
|
||||
}); |
|
||||
|
|
||||
it('should not remove the main div element', done => { |
|
||||
spectator.query('#content').appendChild(document.createElement('div')); |
|
||||
|
|
||||
setTimeout(() => { |
|
||||
expect(spectator.query('#main')).toBeTruthy(); |
|
||||
done(); |
|
||||
}, 100); |
|
||||
}); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,34 +0,0 @@ |
|||||
import { Component, ComponentFactoryResolver, ComponentRef } from '@angular/core'; |
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; |
|
||||
import { ModalContainerComponent } from '../components/modal/modal-container.component'; |
|
||||
|
|
||||
describe('ModalContainerComponent', () => { |
|
||||
@Component({ template: '<div class="foo">bar</div>' }) |
|
||||
class TestComponent {} |
|
||||
|
|
||||
let componentRef: ComponentRef<TestComponent>; |
|
||||
let spectator: Spectator<ModalContainerComponent>; |
|
||||
|
|
||||
const createComponent = createComponentFactory({ |
|
||||
component: ModalContainerComponent, |
|
||||
entryComponents: [TestComponent], |
|
||||
}); |
|
||||
|
|
||||
beforeEach(() => (spectator = createComponent())); |
|
||||
|
|
||||
afterEach(() => componentRef.destroy()); |
|
||||
|
|
||||
describe('#container', () => { |
|
||||
it('should be a ViewContainerRef', () => { |
|
||||
let foo = document.querySelector('div.foo'); |
|
||||
expect(foo).toBeNull(); |
|
||||
|
|
||||
const cfResolver = spectator.inject(ComponentFactoryResolver); |
|
||||
const factory = cfResolver.resolveComponentFactory(TestComponent); |
|
||||
componentRef = spectator.component.container.createComponent(factory); |
|
||||
|
|
||||
foo = document.querySelector('div.foo'); |
|
||||
expect(foo.textContent).toBe('bar'); |
|
||||
}); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,87 +0,0 @@ |
|||||
import { Component, TemplateRef, ViewChild } from '@angular/core'; |
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; |
|
||||
import { ModalContainerComponent } from '../components/modal/modal-container.component'; |
|
||||
import { ModalService } from '../services'; |
|
||||
|
|
||||
describe('ModalContainerComponent', () => { |
|
||||
@Component({ |
|
||||
template: ` |
|
||||
<ng-template #ref> |
|
||||
<div class="foo">bar</div> |
|
||||
</ng-template> |
|
||||
`,
|
|
||||
}) |
|
||||
class TestComponent { |
|
||||
@ViewChild('ref', { static: true }) |
|
||||
template: TemplateRef<any>; |
|
||||
|
|
||||
constructor(public modalService: ModalService) {} |
|
||||
} |
|
||||
|
|
||||
let spectator: Spectator<TestComponent>; |
|
||||
let service: ModalService; |
|
||||
|
|
||||
const createComponent = createComponentFactory({ |
|
||||
component: TestComponent, |
|
||||
entryComponents: [ModalContainerComponent], |
|
||||
}); |
|
||||
|
|
||||
beforeEach(() => { |
|
||||
spectator = createComponent(); |
|
||||
service = spectator.component.modalService; |
|
||||
}); |
|
||||
|
|
||||
afterEach(() => { |
|
||||
service.getContainer().clear(); |
|
||||
service['containerComponentRef'].changeDetectorRef.detectChanges(); |
|
||||
service['containerComponentRef'].destroy(); |
|
||||
}); |
|
||||
|
|
||||
describe('#getContainer', () => { |
|
||||
it('should return the ViewContainerRef of ModalContainerComponent', () => { |
|
||||
let foo = document.querySelector('div.foo'); |
|
||||
expect(foo).toBeNull(); |
|
||||
|
|
||||
const containerRef = service.getContainer(); |
|
||||
const embeddedViewRef = containerRef.createEmbeddedView(spectator.component.template); |
|
||||
|
|
||||
foo = document.querySelector('div.foo'); |
|
||||
expect(foo).toBe(embeddedViewRef.rootNodes[0]); |
|
||||
expect(foo.textContent).toBe('bar'); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
describe('#renderTemplate', () => { |
|
||||
it('should render given template using the ViewContainerRef of ModalContainerComponent', () => { |
|
||||
let foo = document.querySelector('div.foo'); |
|
||||
expect(foo).toBeNull(); |
|
||||
|
|
||||
service.renderTemplate(spectator.component.template); |
|
||||
|
|
||||
foo = document.querySelector('div.foo'); |
|
||||
expect(foo.textContent).toBe('bar'); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
describe('#detectChanges', () => { |
|
||||
it('should call detectChanges on the containerComponentRef', () => { |
|
||||
const spy = jest.spyOn(service['containerComponentRef'].changeDetectorRef, 'detectChanges'); |
|
||||
|
|
||||
service.detectChanges(); |
|
||||
|
|
||||
expect(spy).toHaveBeenCalledTimes(1); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
describe('#clearModal', () => { |
|
||||
it('should call clear on the ViewContainerRef and detectChanges', () => { |
|
||||
const clear = jest.spyOn(service.getContainer(), 'clear'); |
|
||||
const detectChanges = jest.spyOn(service, 'detectChanges'); |
|
||||
|
|
||||
service.clearModal(); |
|
||||
|
|
||||
expect(clear).toHaveBeenCalledTimes(1); |
|
||||
expect(detectChanges).toHaveBeenCalledTimes(1); |
|
||||
}); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,46 +0,0 @@ |
|||||
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; |
|
||||
import { SortOrderIconComponent } from '../components/sort-order-icon/sort-order-icon.component'; |
|
||||
|
|
||||
describe('SortOrderIconComponent', () => { |
|
||||
let spectator: SpectatorHost<SortOrderIconComponent>; |
|
||||
let component: SortOrderIconComponent; |
|
||||
const createHost = createHostFactory(SortOrderIconComponent); |
|
||||
|
|
||||
beforeEach(() => { |
|
||||
spectator = createHost( |
|
||||
'<abp-sort-order-icon sortKey="testKey" [(selectedSortKey)]="selectedSortKey" [(order)]="order"></abp-sort-order-icon>', |
|
||||
{ |
|
||||
hostProps: { |
|
||||
selectedSortKey: '', |
|
||||
order: '', |
|
||||
}, |
|
||||
}, |
|
||||
); |
|
||||
component = spectator.component; |
|
||||
}); |
|
||||
|
|
||||
test('should have correct icon class when selectedSortKey and sortKey are the same', () => { |
|
||||
const newKey = 'testKey'; |
|
||||
component.sort(newKey); |
|
||||
expect(component.selectedSortKey).toBe(newKey); |
|
||||
expect(component.order).toBe('asc'); |
|
||||
expect(component.icon).toBe('sorting_asc'); |
|
||||
}); |
|
||||
|
|
||||
test("shouldn't have any icon class when sortKey and selectedSortKey are different", () => { |
|
||||
const newKey = 'otherKey'; |
|
||||
component.sort(newKey); |
|
||||
expect(component.selectedSortKey).toBe(newKey); |
|
||||
expect(component.order).toBe('asc'); |
|
||||
expect(component.icon).toBe('sorting'); |
|
||||
}); |
|
||||
|
|
||||
test('should change order correctly when sort function called', () => { |
|
||||
component.sort('testKey'); |
|
||||
expect(component.order).toBe('asc'); |
|
||||
component.sort('testKey'); |
|
||||
expect(component.order).toBe('desc'); |
|
||||
component.sort('testKey'); |
|
||||
expect(component.order).toBe(''); |
|
||||
}); |
|
||||
}); |
|
||||
@ -1,6 +1,5 @@ |
|||||
import { InjectionToken } from '@angular/core'; |
import { InjectionToken } from '@angular/core'; |
||||
|
|
||||
// TODO: Should be documented
|
|
||||
export const SUPPRESS_UNSAVED_CHANGES_WARNING = new InjectionToken<boolean>( |
export const SUPPRESS_UNSAVED_CHANGES_WARNING = new InjectionToken<boolean>( |
||||
'SUPPRESS_UNSAVED_CHANGES_WARNING', |
'SUPPRESS_UNSAVED_CHANGES_WARNING', |
||||
); |
); |
||||
|
|||||
Loading…
Reference in new issue