Remainder of initial HttpClient features plus E2E tests
This commit is contained in:
parent
ea3a18af25
commit
8590f6e7a5
|
|
@ -168,19 +168,25 @@ export class BrowserRenderer {
|
||||||
const browserRendererId = this.browserRendererId;
|
const browserRendererId = this.browserRendererId;
|
||||||
const eventHandlerId = renderTreeFrame.attributeEventHandlerId(attributeFrame);
|
const eventHandlerId = renderTreeFrame.attributeEventHandlerId(attributeFrame);
|
||||||
|
|
||||||
|
if (attributeName === 'value') {
|
||||||
|
if (this.tryApplyValueProperty(toDomElement, renderTreeFrame.attributeValue(attributeFrame))) {
|
||||||
|
return; // If this DOM element type has special 'value' handling, don't also write it as an attribute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Instead of applying separate event listeners to each DOM element, use event delegation
|
// TODO: Instead of applying separate event listeners to each DOM element, use event delegation
|
||||||
// and remove all the _blazor*Listener hacks
|
// and remove all the _blazor*Listener hacks
|
||||||
switch (attributeName) {
|
switch (attributeName) {
|
||||||
case 'onclick': {
|
case 'onclick': {
|
||||||
toDomElement.removeEventListener('click', toDomElement['_blazorClickListener']);
|
toDomElement.removeEventListener('click', toDomElement['_blazorClickListener']);
|
||||||
const listener = () => raiseEvent(browserRendererId, componentId, eventHandlerId, 'mouse', { Type: 'click' });
|
const listener = evt => raiseEvent(evt, browserRendererId, componentId, eventHandlerId, 'mouse', { Type: 'click' });
|
||||||
toDomElement['_blazorClickListener'] = listener;
|
toDomElement['_blazorClickListener'] = listener;
|
||||||
toDomElement.addEventListener('click', listener);
|
toDomElement.addEventListener('click', listener);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'onchange': {
|
case 'onchange': {
|
||||||
toDomElement.removeEventListener('change', toDomElement['_blazorChangeListener']);
|
toDomElement.removeEventListener('change', toDomElement['_blazorChangeListener']);
|
||||||
const listener = evt => raiseEvent(browserRendererId, componentId, eventHandlerId, 'change', { Type: 'change', Value: evt.target.value });
|
const listener = evt => raiseEvent(evt, browserRendererId, componentId, eventHandlerId, 'change', { Type: 'change', Value: evt.target.value });
|
||||||
toDomElement['_blazorChangeListener'] = listener;
|
toDomElement['_blazorChangeListener'] = listener;
|
||||||
toDomElement.addEventListener('change', listener);
|
toDomElement.addEventListener('change', listener);
|
||||||
break;
|
break;
|
||||||
|
|
@ -192,7 +198,7 @@ export class BrowserRenderer {
|
||||||
// just to establish that we can pass parameters when raising events.
|
// just to establish that we can pass parameters when raising events.
|
||||||
// We use C#-style PascalCase on the eventInfo to simplify deserialization, but this could
|
// We use C#-style PascalCase on the eventInfo to simplify deserialization, but this could
|
||||||
// change if we introduced a richer JSON library on the .NET side.
|
// change if we introduced a richer JSON library on the .NET side.
|
||||||
raiseEvent(browserRendererId, componentId, eventHandlerId, 'keyboard', { Type: evt.type, Key: (evt as any).key });
|
raiseEvent(evt, browserRendererId, componentId, eventHandlerId, 'keyboard', { Type: evt.type, Key: (evt as any).key });
|
||||||
};
|
};
|
||||||
toDomElement['_blazorKeypressListener'] = listener;
|
toDomElement['_blazorKeypressListener'] = listener;
|
||||||
toDomElement.addEventListener('keypress', listener);
|
toDomElement.addEventListener('keypress', listener);
|
||||||
|
|
@ -208,6 +214,18 @@ export class BrowserRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tryApplyValueProperty(element: Element, value: string | null) {
|
||||||
|
// Certain elements have built-in behaviour for their 'value' property
|
||||||
|
switch (element.tagName) {
|
||||||
|
case 'INPUT':
|
||||||
|
case 'SELECT': // Note: this doen't handle <select> correctly: https://github.com/aspnet/Blazor/issues/157
|
||||||
|
(element as any).value = value;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
insertFrameRange(componentId: number, parent: Element, childIndex: number, frames: System_Array<RenderTreeFramePointer>, startIndex: number, endIndexExcl: number): number {
|
insertFrameRange(componentId: number, parent: Element, childIndex: number, frames: System_Array<RenderTreeFramePointer>, startIndex: number, endIndexExcl: number): number {
|
||||||
const origChildIndex = childIndex;
|
const origChildIndex = childIndex;
|
||||||
for (let index = startIndex; index < endIndexExcl; index++) {
|
for (let index = startIndex; index < endIndexExcl; index++) {
|
||||||
|
|
@ -243,7 +261,9 @@ function removeAttributeFromDOM(parent: Element, childIndex: number, attributeNa
|
||||||
element.removeAttribute(attributeName);
|
element.removeAttribute(attributeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
function raiseEvent(browserRendererId: number, componentId: number, eventHandlerId: number, eventInfoType: EventInfoType, eventInfo: any) {
|
function raiseEvent(event: Event, browserRendererId: number, componentId: number, eventHandlerId: number, eventInfoType: EventInfoType, eventInfo: any) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
if (!raiseEventMethod) {
|
if (!raiseEventMethod) {
|
||||||
raiseEventMethod = platform.findMethod(
|
raiseEventMethod = platform.findMethod(
|
||||||
'Microsoft.AspNetCore.Blazor.Browser', 'Microsoft.AspNetCore.Blazor.Browser.Rendering', 'BrowserRendererEventDispatcher', 'DispatchEvent'
|
'Microsoft.AspNetCore.Blazor.Browser', 'Microsoft.AspNetCore.Blazor.Browser.Rendering', 'BrowserRendererEventDispatcher', 'DispatchEvent'
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,61 @@
|
||||||
import { registerFunction } from '../Interop/RegisteredFunction';
|
import { registerFunction } from '../Interop/RegisteredFunction';
|
||||||
import { platform } from '../Environment';
|
import { platform } from '../Environment';
|
||||||
import { MethodHandle } from '../Platform/Platform';
|
import { MethodHandle, System_String } from '../Platform/Platform';
|
||||||
const httpClientAssembly = 'Microsoft.AspNetCore.Blazor.Browser';
|
const httpClientAssembly = 'Microsoft.AspNetCore.Blazor.Browser';
|
||||||
const httpClientNamespace = `${httpClientAssembly}.Services.Temporary`;
|
const httpClientNamespace = `${httpClientAssembly}.Services.Temporary`;
|
||||||
const httpClientTypeName = 'HttpClient';
|
const httpClientTypeName = 'HttpClient';
|
||||||
const httpClientFullTypeName = `${httpClientNamespace}.${httpClientTypeName}`;
|
const httpClientFullTypeName = `${httpClientNamespace}.${httpClientTypeName}`;
|
||||||
let receiveResponseMethod: MethodHandle;
|
let receiveResponseMethod: MethodHandle;
|
||||||
|
|
||||||
registerFunction(`${httpClientFullTypeName}.Send`, (id: number, requestUri: string) => {
|
registerFunction(`${httpClientFullTypeName}.Send`, (id: number, method: string, requestUri: string, body: string | null, headersJson: string | null) => {
|
||||||
sendAsync(id, requestUri);
|
sendAsync(id, method, requestUri, body, headersJson);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function sendAsync(id: number, requestUri: string) {
|
async function sendAsync(id: number, method: string, requestUri: string, body: string | null, headersJson: string | null) {
|
||||||
|
let response: Response;
|
||||||
|
let responseText: string;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(requestUri);
|
response = await fetch(requestUri, {
|
||||||
const responseText = await response.text();
|
method: method,
|
||||||
dispatchResponse(id, response.status, responseText, null);
|
body: body,
|
||||||
|
headers: headersJson ? (JSON.parse(headersJson) as string[][]) : undefined
|
||||||
|
});
|
||||||
|
responseText = await response.text();
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
dispatchResponse(id, 0, null, ex.toString());
|
dispatchErrorResponse(id, ex.toString());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatchSuccessResponse(id, response, responseText);
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatchResponse(id: number, statusCode: number, responseText: string | null, errorInfo: string | null) {
|
function dispatchSuccessResponse(id: number, response: Response, responseText: string) {
|
||||||
|
const responseDescriptor: ResponseDescriptor = {
|
||||||
|
StatusCode: response.status,
|
||||||
|
Headers: []
|
||||||
|
};
|
||||||
|
response.headers.forEach((value, name) => {
|
||||||
|
responseDescriptor.Headers.push([name, value]);
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatchResponse(
|
||||||
|
id,
|
||||||
|
platform.toDotNetString(JSON.stringify(responseDescriptor)),
|
||||||
|
platform.toDotNetString(responseText), // TODO: Consider how to handle non-string responses
|
||||||
|
/* errorMessage */ null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchErrorResponse(id: number, errorMessage: string) {
|
||||||
|
dispatchResponse(
|
||||||
|
id,
|
||||||
|
/* responseDescriptor */ null,
|
||||||
|
/* responseText */ null,
|
||||||
|
platform.toDotNetString(errorMessage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchResponse(id: number, responseDescriptor: System_String | null, responseText: System_String | null, errorMessage: System_String | null) {
|
||||||
if (!receiveResponseMethod) {
|
if (!receiveResponseMethod) {
|
||||||
receiveResponseMethod = platform.findMethod(
|
receiveResponseMethod = platform.findMethod(
|
||||||
httpClientAssembly,
|
httpClientAssembly,
|
||||||
|
|
@ -30,11 +64,20 @@ function dispatchResponse(id: number, statusCode: number, responseText: string |
|
||||||
'ReceiveResponse'
|
'ReceiveResponse'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.callMethod(receiveResponseMethod, null, [
|
platform.callMethod(receiveResponseMethod, null, [
|
||||||
platform.toDotNetString(id.toString()),
|
platform.toDotNetString(id.toString()),
|
||||||
platform.toDotNetString(statusCode.toString()),
|
responseDescriptor,
|
||||||
responseText === null ? null : platform.toDotNetString(responseText),
|
responseText,
|
||||||
errorInfo === null ? null : platform.toDotNetString(errorInfo.toString())
|
errorMessage,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep this in sync with the .NET equivalent in HttpClient.cs
|
||||||
|
interface ResponseDescriptor {
|
||||||
|
// We don't have BodyText in here because if we did, then in the JSON-response case (which
|
||||||
|
// is the most common case), we'd be double-encoding it, since the entire ResponseDescriptor
|
||||||
|
// also gets JSON encoded. It would work but is twice the amount of string processing.
|
||||||
|
StatusCode: number;
|
||||||
|
Headers: string[][];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Blazor.Browser.Interop;
|
using Microsoft.AspNetCore.Blazor.Browser.Interop;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
|
namespace Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
|
||||||
|
|
@ -50,7 +53,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
|
||||||
return await response.Content.ReadAsStringAsync();
|
return await response.Content.ReadAsStringAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// <summary>
|
/// <summary>
|
||||||
/// Sends a GET request to the specified URI and returns the response as
|
/// Sends a GET request to the specified URI and returns the response as
|
||||||
/// an instance of <see cref="HttpResponseMessage"/> in an asynchronous
|
/// an instance of <see cref="HttpResponseMessage"/> in an asynchronous
|
||||||
/// operation.
|
/// operation.
|
||||||
|
|
@ -58,6 +61,30 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
|
||||||
/// <param name="requestUri">The URI the request is sent to.</param>
|
/// <param name="requestUri">The URI the request is sent to.</param>
|
||||||
/// <returns>A task representing the asynchronous operation.</returns>
|
/// <returns>A task representing the asynchronous operation.</returns>
|
||||||
public Task<HttpResponseMessage> GetAsync(string requestUri)
|
public Task<HttpResponseMessage> GetAsync(string requestUri)
|
||||||
|
=> SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a POST request to the specified URI and returns the response as
|
||||||
|
/// an instance of <see cref="HttpResponseMessage"/> in an asynchronous
|
||||||
|
/// operation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestUri">The URI the request is sent to.</param>
|
||||||
|
/// <param name="content">The content for the request.</param>
|
||||||
|
/// <returns>A task representing the asynchronous operation.</returns>
|
||||||
|
public Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content)
|
||||||
|
=> SendAsync(new HttpRequestMessage(HttpMethod.Post, requestUri)
|
||||||
|
{
|
||||||
|
Content = content
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends an HTTP request to the specified URI and returns the response as
|
||||||
|
/// an instance of <see cref="HttpResponseMessage"/> in an asynchronous
|
||||||
|
/// operation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The request to be sent.</param>
|
||||||
|
/// <returns>A task representing the asynchronous operation.</returns>
|
||||||
|
public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<HttpResponseMessage>();
|
var tcs = new TaskCompletionSource<HttpResponseMessage>();
|
||||||
int id;
|
int id;
|
||||||
|
|
@ -67,12 +94,36 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
|
||||||
_pendingRequests.Add(id, tcs);
|
_pendingRequests.Add(id, tcs);
|
||||||
}
|
}
|
||||||
|
|
||||||
RegisteredFunction.Invoke<object>($"{typeof(HttpClient).FullName}.Send", id, requestUri);
|
RegisteredFunction.Invoke<object>(
|
||||||
|
$"{typeof(HttpClient).FullName}.Send",
|
||||||
|
id,
|
||||||
|
request.Method.Method,
|
||||||
|
request.RequestUri.ToString(),
|
||||||
|
request.Content == null ? null : await GetContentAsString(request.Content),
|
||||||
|
SerializeHeadersAsJson(request));
|
||||||
|
|
||||||
return tcs.Task;
|
return await tcs.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ReceiveResponse(string id, string statusCode, string responseText, string errorText)
|
private string SerializeHeadersAsJson(HttpRequestMessage request)
|
||||||
|
=> Json.Serialize(
|
||||||
|
(from header in request.Headers.Concat(request.Content?.Headers ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>())
|
||||||
|
from headerValue in header.Value // There can be more than one value for each name
|
||||||
|
select new[] { header.Key, headerValue }).ToList()
|
||||||
|
);
|
||||||
|
|
||||||
|
private static async Task<string> GetContentAsString(HttpContent content)
|
||||||
|
=> content is StringContent stringContent
|
||||||
|
? await stringContent.ReadAsStringAsync()
|
||||||
|
: throw new InvalidOperationException($"Currently, {typeof(HttpClient).FullName} " +
|
||||||
|
$"only supports contents of type {nameof(StringContent)}, but you supplied " +
|
||||||
|
$"{content.GetType().FullName}.");
|
||||||
|
|
||||||
|
private static void ReceiveResponse(
|
||||||
|
string id,
|
||||||
|
string responseDescriptorJson,
|
||||||
|
string responseBodyText,
|
||||||
|
string errorText)
|
||||||
{
|
{
|
||||||
TaskCompletionSource<HttpResponseMessage> tcs;
|
TaskCompletionSource<HttpResponseMessage> tcs;
|
||||||
var idVal = int.Parse(id);
|
var idVal = int.Parse(id);
|
||||||
|
|
@ -82,17 +133,42 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
|
||||||
_pendingRequests.Remove(idVal);
|
_pendingRequests.Remove(idVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorText == null)
|
if (errorText != null)
|
||||||
{
|
{
|
||||||
tcs.SetResult(new HttpResponseMessage
|
tcs.SetException(new HttpRequestException(errorText));
|
||||||
{
|
|
||||||
StatusCode = (HttpStatusCode)int.Parse(statusCode),
|
|
||||||
Content = new StringContent(responseText)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
tcs.SetException(new HttpRequestException(errorText));
|
var responseDescriptor = Json.Deserialize<ResponseDescriptor>(responseDescriptorJson);
|
||||||
|
var responseContent = responseBodyText == null ? null : new StringContent(responseBodyText);
|
||||||
|
var responseMessage = responseDescriptor.ToResponseMessage(responseContent);
|
||||||
|
tcs.SetResult(responseMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep in sync with TypeScript class in Http.ts
|
||||||
|
private class ResponseDescriptor
|
||||||
|
{
|
||||||
|
#pragma warning disable 0649
|
||||||
|
public int StatusCode { get; set; }
|
||||||
|
public string[][] Headers { get; set; }
|
||||||
|
#pragma warning restore 0649
|
||||||
|
|
||||||
|
public HttpResponseMessage ToResponseMessage(HttpContent content)
|
||||||
|
{
|
||||||
|
var result = new HttpResponseMessage((HttpStatusCode)StatusCode);
|
||||||
|
result.Content = content;
|
||||||
|
var headers = result.Headers;
|
||||||
|
var contentHeaders = result.Content?.Headers;
|
||||||
|
foreach (var pair in Headers)
|
||||||
|
{
|
||||||
|
if (!headers.TryAddWithoutValidation(pair[0], pair[1]))
|
||||||
|
{
|
||||||
|
contentHeaders?.TryAddWithoutValidation(pair[0], pair[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.1" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
|
||||||
|
<PackageReference Include="Selenium.Support" Version="3.8.0" />
|
||||||
<PackageReference Include="Selenium.WebDriver" Version="3.8.0" />
|
<PackageReference Include="Selenium.WebDriver" Version="3.8.0" />
|
||||||
<PackageReference Include="xunit" Version="2.3.1" />
|
<PackageReference Include="xunit" Version="2.3.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using BasicTestApp.HttpClientTest;
|
||||||
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure;
|
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures;
|
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures;
|
||||||
|
using OpenQA.Selenium;
|
||||||
|
using OpenQA.Selenium.Support.UI;
|
||||||
using System;
|
using System;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -13,6 +16,10 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
||||||
public class HttpClientTest : BasicTestAppTestBase, IClassFixture<AspNetSiteServerFixture>
|
public class HttpClientTest : BasicTestAppTestBase, IClassFixture<AspNetSiteServerFixture>
|
||||||
{
|
{
|
||||||
readonly ServerFixture _apiServerFixture;
|
readonly ServerFixture _apiServerFixture;
|
||||||
|
readonly IWebElement _appElement;
|
||||||
|
IWebElement _responseStatus;
|
||||||
|
IWebElement _responseBody;
|
||||||
|
IWebElement _responseHeaders;
|
||||||
|
|
||||||
public HttpClientTest(
|
public HttpClientTest(
|
||||||
BrowserFixture browserFixture,
|
BrowserFixture browserFixture,
|
||||||
|
|
@ -23,7 +30,8 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
||||||
apiServerFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
apiServerFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
||||||
_apiServerFixture = apiServerFixture;
|
_apiServerFixture = apiServerFixture;
|
||||||
|
|
||||||
//Navigate(ServerPathBase, noReload: true);
|
Navigate(ServerPathBase, noReload: true);
|
||||||
|
_appElement = MountTestComponent<HttpRequestsComponent>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -36,5 +44,91 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
||||||
var responseText = await httpClient.GetStringAsync("/api/greeting/sayhello");
|
var responseText = await httpClient.GetStringAsync("/api/greeting/sayhello");
|
||||||
Assert.Equal("Hello", responseText);
|
Assert.Equal("Hello", responseText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanPerformGetRequest()
|
||||||
|
{
|
||||||
|
IssueRequest("GET", "/api/person");
|
||||||
|
Assert.Equal("OK", _responseStatus.Text);
|
||||||
|
Assert.Equal("[\"value1\",\"value2\"]", _responseBody.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanPerformPostRequestWithBody()
|
||||||
|
{
|
||||||
|
var testMessage = $"The value is {Guid.NewGuid()}";
|
||||||
|
IssueRequest("POST", "/api/person", testMessage);
|
||||||
|
Assert.Equal("OK", _responseStatus.Text);
|
||||||
|
Assert.Equal($"You posted: {testMessage}", _responseBody.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanReadResponseHeaders()
|
||||||
|
{
|
||||||
|
IssueRequest("GET", "/api/person");
|
||||||
|
Assert.Equal("OK", _responseStatus.Text);
|
||||||
|
|
||||||
|
// Note that we only see header names case insensitively. The 'fetch' API
|
||||||
|
// can use whatever casing rules it wants, because the HTTP spec says the
|
||||||
|
// names are case-insensitive. In practice, Chrome lowercases them all.
|
||||||
|
// Ideally we should make the test case-insensitive for header name, but
|
||||||
|
// case-sensitive for header value.
|
||||||
|
Assert.Contains("mycustomheader: My custom value", _responseHeaders.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanSendRequestHeaders()
|
||||||
|
{
|
||||||
|
AddRequestHeader("TestHeader", "Value from test");
|
||||||
|
AddRequestHeader("another-header", "Another value");
|
||||||
|
IssueRequest("DELETE", "/api/person");
|
||||||
|
Assert.Equal("OK", _responseStatus.Text);
|
||||||
|
Assert.Contains("TestHeader: Value from test", _responseBody.Text);
|
||||||
|
Assert.Contains("another-header: Another value", _responseBody.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanSendAndReceiveJson()
|
||||||
|
{
|
||||||
|
AddRequestHeader("Content-Type", "application/json");
|
||||||
|
IssueRequest("PUT", "/api/person", "{\"Name\": \"Bert\", \"Id\": 123}");
|
||||||
|
Assert.Equal("OK", _responseStatus.Text);
|
||||||
|
Assert.Contains("Content-Type: application/json", _responseHeaders.Text);
|
||||||
|
Assert.Equal("{\"id\":123,\"name\":\"Bert\"}", _responseBody.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void IssueRequest(string requestMethod, string relativeUri, string requestBody = null)
|
||||||
|
{
|
||||||
|
var targetUri = new Uri(_apiServerFixture.RootUri, relativeUri);
|
||||||
|
SetValue("request-uri", targetUri.AbsoluteUri);
|
||||||
|
SetValue("request-body", requestBody ?? string.Empty);
|
||||||
|
new SelectElement(Browser.FindElement(By.Id("request-method")))
|
||||||
|
.SelectByText(requestMethod);
|
||||||
|
|
||||||
|
_appElement.FindElement(By.Id("send-request")).Click();
|
||||||
|
|
||||||
|
new WebDriverWait(Browser, TimeSpan.FromSeconds(30)).Until(
|
||||||
|
driver => driver.FindElement(By.Id("response-status")) != null);
|
||||||
|
_responseStatus = _appElement.FindElement(By.Id("response-status"));
|
||||||
|
_responseBody = _appElement.FindElement(By.Id("response-body"));
|
||||||
|
_responseHeaders = _appElement.FindElement(By.Id("response-headers"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddRequestHeader(string name, string value)
|
||||||
|
{
|
||||||
|
var addHeaderButton = _appElement.FindElement(By.Id("add-header"));
|
||||||
|
addHeaderButton.Click();
|
||||||
|
var newHeaderEntry = _appElement.FindElement(By.CssSelector(".header-entry:last-of-type"));
|
||||||
|
var textBoxes = newHeaderEntry.FindElements(By.TagName("input"));
|
||||||
|
textBoxes[0].SendKeys(name);
|
||||||
|
textBoxes[1].SendKeys(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetValue(string elementId, string value)
|
||||||
|
{
|
||||||
|
var element = Browser.FindElement(By.Id(elementId));
|
||||||
|
element.Clear();
|
||||||
|
element.SendKeys(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
@using System.Net
|
||||||
|
@using System.Net.Http
|
||||||
|
@inject Microsoft.AspNetCore.Blazor.Browser.Services.Temporary.HttpClient Http
|
||||||
|
|
||||||
|
<h1>HTTP request tester</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<div>URI:</div>
|
||||||
|
<input id="request-uri" value=@uri @onchange(value => { uri = (string)value; }) size="60"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<div>Method:</div>
|
||||||
|
<select id="request-method" value=@method @onchange(value => { method = (string)value; })>
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
<option value="DELETE">DELETE</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<div>Request body:</div>
|
||||||
|
<textarea id="request-body" value=@requestBody @onchange(value => { requestBody = (string)value; })></textarea>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<div>Request headers:</div>
|
||||||
|
@foreach (var header in requestHeaders)
|
||||||
|
{
|
||||||
|
<div class="header-entry">
|
||||||
|
Name: <input value=@header.Name @onchange(value => { header.Name = (string)value; }) />
|
||||||
|
Value: <input value=@header.Value @onchange(value => { header.Value = (string)value; }) />
|
||||||
|
[<a href="#" @onclick(() => RemoveHeader(header))>remove</a>]
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button id="add-header" @onclick(AddHeader)>Add</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button id="send-request" @onclick(DoRequest)>Request</button>
|
||||||
|
|
||||||
|
@if (responseStatusCode.HasValue)
|
||||||
|
{
|
||||||
|
<h2>Response</h2>
|
||||||
|
<p><div>Status:</div><span id="response-status">@responseStatusCode</span></p>
|
||||||
|
<p><div>Body:</div><textarea id="response-body" readonly>@responseBody</textarea></p>
|
||||||
|
<p><div>Headers:</div><textarea id="response-headers" readonly>@responseHeaders</textarea></p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
@functions {
|
||||||
|
string uri = "http://api.icndb.com/jokes/random";
|
||||||
|
string method = "GET";
|
||||||
|
string requestBody = "";
|
||||||
|
List<RequestHeader> requestHeaders = new List<RequestHeader>();
|
||||||
|
|
||||||
|
HttpStatusCode? responseStatusCode;
|
||||||
|
string responseBody;
|
||||||
|
string responseHeaders;
|
||||||
|
|
||||||
|
async void DoRequest()
|
||||||
|
{
|
||||||
|
responseStatusCode = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var requestMessage = new HttpRequestMessage()
|
||||||
|
{
|
||||||
|
Method = new HttpMethod(method),
|
||||||
|
RequestUri = new Uri(uri),
|
||||||
|
Content = string.IsNullOrEmpty(requestBody)
|
||||||
|
? null
|
||||||
|
: new StringContent(requestBody)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var header in requestHeaders)
|
||||||
|
{
|
||||||
|
if (!requestMessage.Headers.TryAddWithoutValidation(header.Name, header.Value))
|
||||||
|
{
|
||||||
|
requestMessage.Content?.Headers.TryAddWithoutValidation(header.Name, header.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await Http.SendAsync(requestMessage);
|
||||||
|
responseStatusCode = response.StatusCode;
|
||||||
|
responseBody = await response.Content.ReadAsStringAsync();
|
||||||
|
var allHeaders = response.Headers.Concat(response.Content?.Headers
|
||||||
|
?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>());
|
||||||
|
responseHeaders = string.Join(
|
||||||
|
Environment.NewLine,
|
||||||
|
allHeaders.Select(pair => $"{pair.Key}: {pair.Value.First()}").ToArray());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (ex is AggregateException)
|
||||||
|
{
|
||||||
|
ex = ex.InnerException;
|
||||||
|
}
|
||||||
|
responseStatusCode = HttpStatusCode.SeeOther;
|
||||||
|
responseBody = ex.Message + Environment.NewLine + ex.StackTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddHeader()
|
||||||
|
=> requestHeaders.Add(new RequestHeader());
|
||||||
|
|
||||||
|
void RemoveHeader(RequestHeader header)
|
||||||
|
=> requestHeaders.Remove(header);
|
||||||
|
|
||||||
|
class RequestHeader
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
@using Microsoft.AspNetCore.Blazor.Routing
|
@using Microsoft.AspNetCore.Blazor.Routing
|
||||||
<c:Router AppAssembly=@(typeof(BasicTestApp.Program).Assembly)
|
<c:Router AppAssembly=@(typeof(BasicTestApp.Program).Assembly)
|
||||||
PagesNamespace=@nameof(BasicTestApp)
|
PagesNamespace=@nameof(BasicTestApp)
|
||||||
DefaultComponentName=@("Default") />
|
DefaultComponentName=@("Default") />
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,44 @@
|
||||||
<base href="/subdir/" />
|
<base href="/subdir/" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="test-selector" style="display: none">
|
||||||
|
Select test:
|
||||||
|
<select onchange="mountTestComponent(event.target.value)">
|
||||||
|
<option value="">Choose...</option>
|
||||||
|
<option value="BasicTestApp.AddRemoveChildComponents">Add/remove child components</option>
|
||||||
|
<option value="BasicTestApp.CounterComponent">Counter</option>
|
||||||
|
<option value="BasicTestApp.CounterComponentUsingChild">Counter using child component</option>
|
||||||
|
<option value="BasicTestApp.CounterComponentWrapper">Counter wrapped in parent</option>
|
||||||
|
<option value="BasicTestApp.KeyPressEventComponent">Key press event</option>
|
||||||
|
<option value="BasicTestApp.ParentChildComponent">Parent component with child</option>
|
||||||
|
<option value="BasicTestApp.PropertiesChangedHandlerParent">Parent component that changes parameters on child</option>
|
||||||
|
<option value="BasicTestApp.RedTextComponent">Red text</option>
|
||||||
|
<option value="BasicTestApp.RenderFragmentToggler">Render fragment renderer</option>
|
||||||
|
<option value="BasicTestApp.TextOnlyComponent">Plain text</option>
|
||||||
|
<option value="BasicTestApp.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
|
||||||
|
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
|
||||||
|
<!--<option value="BasicTestApp.RouterTest.Default">Router</option> Excluded because it requires additional setup to work correctly when loaded manually -->
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<span id="source-info"></span>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
|
||||||
<app>Loading...</app>
|
<app>Loading...</app>
|
||||||
|
|
||||||
<script type="blazor-boot"></script>
|
<script type="blazor-boot"></script>
|
||||||
<script>
|
<script>
|
||||||
// The client-side .NET code calls this when it is ready to be called from test code
|
// The client-side .NET code calls this when it is ready to be called from test code
|
||||||
// The Xunit test code polls until it sees the flag is set
|
// The Xunit test code polls until it sees the flag is set
|
||||||
Blazor.registerFunction('testReady', function () { window.isTestReady = true; });
|
Blazor.registerFunction('testReady', function () {
|
||||||
|
window.isTestReady = true;
|
||||||
|
document.getElementsByTagName('APP')[0].textContent = '';
|
||||||
|
document.getElementById('test-selector').style.display = 'block';
|
||||||
|
});
|
||||||
|
|
||||||
// The Xunit test code calls this when setting up tests for specific components
|
// The Xunit test code calls this when setting up tests for specific components
|
||||||
function mountTestComponent(typeName) {
|
function mountTestComponent(typeName) {
|
||||||
|
document.getElementById('source-info').innerHTML = '<code><tt>' + typeName.replace(/\./g, '/') + '.cshtml</code></strong>';
|
||||||
var method = Blazor.platform.findMethod('BasicTestApp', 'BasicTestApp', 'Program', 'MountTestComponent');
|
var method = Blazor.platform.findMethod('BasicTestApp', 'BasicTestApp', 'Program', 'MountTestComponent');
|
||||||
Blazor.platform.callMethod(method, null, [Blazor.platform.toDotNetString(typeName)]);
|
Blazor.platform.callMethod(method, null, [Blazor.platform.toDotNetString(typeName)]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,60 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace TestServer.Controllers
|
namespace TestServer.Controllers
|
||||||
{
|
{
|
||||||
|
[EnableCors("AllowAll")]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class PersonController : Controller
|
public class PersonController : Controller
|
||||||
{
|
{
|
||||||
// GET api/values
|
// GET api/person
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IEnumerable<string> Get()
|
public IEnumerable<string> Get()
|
||||||
{
|
{
|
||||||
|
HttpContext.Response.Headers.Add("MyCustomHeader", "My custom value");
|
||||||
return new string[] { "value1", "value2" };
|
return new string[] { "value1", "value2" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET api/values/5
|
// POST api/person
|
||||||
[HttpGet("{id}")]
|
|
||||||
public string Get(int id)
|
|
||||||
{
|
|
||||||
return "value";
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST api/values
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public void Post([FromBody]string value)
|
public async Task<string> Post()
|
||||||
{
|
{
|
||||||
|
using (var reader = new StreamReader(Request.Body))
|
||||||
|
{
|
||||||
|
var plainTextBodyContent = await reader.ReadToEndAsync();
|
||||||
|
return $"You posted: {plainTextBodyContent}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT api/values/5
|
// PUT api/person
|
||||||
[HttpPut("{id}")]
|
[HttpPut]
|
||||||
public void Put(int id, [FromBody]string value)
|
public Person Put([FromBody, Required] Person person)
|
||||||
{
|
{
|
||||||
|
return person;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE api/values/5
|
// DELETE api/person
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete]
|
||||||
public void Delete(int id)
|
public string Delete()
|
||||||
{
|
{
|
||||||
|
var result = new StringBuilder();
|
||||||
|
foreach (var header in Request.Headers)
|
||||||
|
{
|
||||||
|
result.AppendLine($"{header.Key}: {string.Join(",", header.Value.ToArray())}");
|
||||||
|
}
|
||||||
|
return "REQUEST HEADERS:\n" + result.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Person
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,17 @@ namespace TestServer
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddMvc();
|
services.AddMvc();
|
||||||
|
services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowAll", builder =>
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.AllowAnyOrigin()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.WithExposedHeaders("MyCustomHeader");
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue