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 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
|
||||
// and remove all the _blazor*Listener hacks
|
||||
switch (attributeName) {
|
||||
case 'onclick': {
|
||||
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.addEventListener('click', listener);
|
||||
break;
|
||||
}
|
||||
case 'onchange': {
|
||||
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.addEventListener('change', listener);
|
||||
break;
|
||||
|
|
@ -192,7 +198,7 @@ export class BrowserRenderer {
|
|||
// 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
|
||||
// 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.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 {
|
||||
const origChildIndex = childIndex;
|
||||
for (let index = startIndex; index < endIndexExcl; index++) {
|
||||
|
|
@ -243,7 +261,9 @@ function removeAttributeFromDOM(parent: Element, childIndex: number, attributeNa
|
|||
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) {
|
||||
raiseEventMethod = platform.findMethod(
|
||||
'Microsoft.AspNetCore.Blazor.Browser', 'Microsoft.AspNetCore.Blazor.Browser.Rendering', 'BrowserRendererEventDispatcher', 'DispatchEvent'
|
||||
|
|
|
|||
|
|
@ -1,27 +1,61 @@
|
|||
import { registerFunction } from '../Interop/RegisteredFunction';
|
||||
import { platform } from '../Environment';
|
||||
import { MethodHandle } from '../Platform/Platform';
|
||||
import { MethodHandle, System_String } from '../Platform/Platform';
|
||||
const httpClientAssembly = 'Microsoft.AspNetCore.Blazor.Browser';
|
||||
const httpClientNamespace = `${httpClientAssembly}.Services.Temporary`;
|
||||
const httpClientTypeName = 'HttpClient';
|
||||
const httpClientFullTypeName = `${httpClientNamespace}.${httpClientTypeName}`;
|
||||
let receiveResponseMethod: MethodHandle;
|
||||
|
||||
registerFunction(`${httpClientFullTypeName}.Send`, (id: number, requestUri: string) => {
|
||||
sendAsync(id, requestUri);
|
||||
registerFunction(`${httpClientFullTypeName}.Send`, (id: number, method: string, requestUri: string, body: string | null, headersJson: string | null) => {
|
||||
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 {
|
||||
const response = await fetch(requestUri);
|
||||
const responseText = await response.text();
|
||||
dispatchResponse(id, response.status, responseText, null);
|
||||
response = await fetch(requestUri, {
|
||||
method: method,
|
||||
body: body,
|
||||
headers: headersJson ? (JSON.parse(headersJson) as string[][]) : undefined
|
||||
});
|
||||
responseText = await response.text();
|
||||
} 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) {
|
||||
receiveResponseMethod = platform.findMethod(
|
||||
httpClientAssembly,
|
||||
|
|
@ -30,11 +64,20 @@ function dispatchResponse(id: number, statusCode: number, responseText: string |
|
|||
'ReceiveResponse'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
platform.callMethod(receiveResponseMethod, null, [
|
||||
platform.toDotNetString(id.toString()),
|
||||
platform.toDotNetString(statusCode.toString()),
|
||||
responseText === null ? null : platform.toDotNetString(responseText),
|
||||
errorInfo === null ? null : platform.toDotNetString(errorInfo.toString())
|
||||
responseDescriptor,
|
||||
responseText,
|
||||
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.
|
||||
|
||||
using Microsoft.AspNetCore.Blazor.Browser.Interop;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
|
||||
|
|
@ -50,7 +53,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
|
|||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
// <summary>
|
||||
/// <summary>
|
||||
/// Sends a GET request to the specified URI and returns the response as
|
||||
/// an instance of <see cref="HttpResponseMessage"/> in an asynchronous
|
||||
/// operation.
|
||||
|
|
@ -58,6 +61,30 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
|
|||
/// <param name="requestUri">The URI the request is sent to.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
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>();
|
||||
int id;
|
||||
|
|
@ -67,12 +94,36 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
|
|||
_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;
|
||||
var idVal = int.Parse(id);
|
||||
|
|
@ -82,17 +133,42 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Services.Temporary
|
|||
_pendingRequests.Remove(idVal);
|
||||
}
|
||||
|
||||
if (errorText == null)
|
||||
if (errorText != null)
|
||||
{
|
||||
tcs.SetResult(new HttpResponseMessage
|
||||
{
|
||||
StatusCode = (HttpStatusCode)int.Parse(statusCode),
|
||||
Content = new StringContent(responseText)
|
||||
});
|
||||
tcs.SetException(new HttpRequestException(errorText));
|
||||
}
|
||||
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.StaticFiles" Version="2.0.1" />
|
||||
<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="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
// 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.
|
||||
|
||||
using BasicTestApp.HttpClientTest;
|
||||
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure;
|
||||
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -13,6 +16,10 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
|||
public class HttpClientTest : BasicTestAppTestBase, IClassFixture<AspNetSiteServerFixture>
|
||||
{
|
||||
readonly ServerFixture _apiServerFixture;
|
||||
readonly IWebElement _appElement;
|
||||
IWebElement _responseStatus;
|
||||
IWebElement _responseBody;
|
||||
IWebElement _responseHeaders;
|
||||
|
||||
public HttpClientTest(
|
||||
BrowserFixture browserFixture,
|
||||
|
|
@ -23,7 +30,8 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
|||
apiServerFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
||||
_apiServerFixture = apiServerFixture;
|
||||
|
||||
//Navigate(ServerPathBase, noReload: true);
|
||||
Navigate(ServerPathBase, noReload: true);
|
||||
_appElement = MountTestComponent<HttpRequestsComponent>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -36,5 +44,91 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
|||
var responseText = await httpClient.GetStringAsync("/api/greeting/sayhello");
|
||||
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
|
||||
<c:Router AppAssembly=@(typeof(BasicTestApp.Program).Assembly)
|
||||
PagesNamespace=@nameof(BasicTestApp)
|
||||
DefaultComponentName=@("Default") />
|
||||
PagesNamespace=@nameof(BasicTestApp)
|
||||
DefaultComponentName=@("Default") />
|
||||
|
|
|
|||
|
|
@ -6,16 +6,44 @@
|
|||
<base href="/subdir/" />
|
||||
</head>
|
||||
<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>
|
||||
|
||||
<script type="blazor-boot"></script>
|
||||
<script>
|
||||
// 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
|
||||
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
|
||||
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');
|
||||
Blazor.platform.callMethod(method, null, [Blazor.platform.toDotNetString(typeName)]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,60 @@
|
|||
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;
|
||||
|
||||
namespace TestServer.Controllers
|
||||
{
|
||||
[EnableCors("AllowAll")]
|
||||
[Route("api/[controller]")]
|
||||
public class PersonController : Controller
|
||||
{
|
||||
// GET api/values
|
||||
// GET api/person
|
||||
[HttpGet]
|
||||
public IEnumerable<string> Get()
|
||||
{
|
||||
HttpContext.Response.Headers.Add("MyCustomHeader", "My custom value");
|
||||
return new string[] { "value1", "value2" };
|
||||
}
|
||||
|
||||
// GET api/values/5
|
||||
[HttpGet("{id}")]
|
||||
public string Get(int id)
|
||||
{
|
||||
return "value";
|
||||
}
|
||||
|
||||
// POST api/values
|
||||
// POST api/person
|
||||
[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
|
||||
[HttpPut("{id}")]
|
||||
public void Put(int id, [FromBody]string value)
|
||||
// PUT api/person
|
||||
[HttpPut]
|
||||
public Person Put([FromBody, Required] Person person)
|
||||
{
|
||||
return person;
|
||||
}
|
||||
|
||||
// DELETE api/values/5
|
||||
[HttpDelete("{id}")]
|
||||
public void Delete(int id)
|
||||
// DELETE api/person
|
||||
[HttpDelete]
|
||||
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)
|
||||
{
|
||||
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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue