Remainder of initial HttpClient features plus E2E tests

This commit is contained in:
Steve Sanderson 2018-02-23 14:43:57 +00:00
parent ea3a18af25
commit 8590f6e7a5
10 changed files with 463 additions and 48 deletions

View File

@ -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'

View File

@ -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[][];
}

View File

@ -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;
} }
} }
} }

View File

@ -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" />

View File

@ -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);
}
} }
} }

View File

@ -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; }
}
}

View File

@ -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") />

View File

@ -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>
&nbsp;
<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)]);
} }

View File

@ -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; }
} }
} }
} }

View File

@ -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.