Prevent dotnet-watch script injection staleness (#27778)

The browser refresh mechansim used by dotnet-watch and VS modifies HTML content. The modified content includes
a script block that has a WebSocket url that changes for each new execution of dotnet watch run (not rebuilds, but watch itself).
HTML content can come from views or static html files on disk. For the latter, ASP.NET Core participates in browser caching by sending (and invalidating) etag headers.

One way to fix this problem is remove or modify the etag headers. The risk here is that might cause differences in behavior in development users may come to rely on that are unavailable in production. This change instead modifies the HTML content so the output is always consistent and consequently safe to cache. The dynamic content is served separately by the injected middleware.

This change fixes the issue of multiple instances of dotnet-watch. While this issue may crop up if you alternate between dotnet run and dotnet watch run but we haven't seen this being an issue as yet.

Fixes #27548

Summary
Running dotnet watch run multiple times in Blazor WASM apps (or any app that serves static html files) can produce console errors and prevent the browser refresh feature from working. Given that we've been telling our users to use dotnet watch run as their primary way to work outside of VS, it's likely more users would run in this.

Customer impact
A hard browser refresh (Ctrl + R) is needed to get the refresh behavior to work.

Regression
No. This has existed since the feature was introduced we did not get reports of it

Risk
Low. The fix is isolated to dotnet-watch and VS's browser refresh mechanism which is in preview. The change was tested locally, but if there's a regression or if the change interferes with user's workflow, users have the ability to disable this feature.
This commit is contained in:
Pranav K 2020-11-13 14:25:47 -08:00 committed by GitHub
parent fc1ebaaaa6
commit b34c5ddd5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 157 additions and 63 deletions

View File

@ -0,0 +1,51 @@
// 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 System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Watch.BrowserRefresh
{
/// <summary>
/// Responds with the contennts of WebSocketScriptInjection.js with the stub WebSocket url replaced by the
/// one specified by the launching app.
/// </summary>
public sealed class BrowserScriptMiddleware
{
private readonly byte[] _scriptBytes;
private readonly string _contentLength;
public BrowserScriptMiddleware(RequestDelegate next)
: this(Environment.GetEnvironmentVariable("ASPNETCORE_AUTO_RELOAD_WS_ENDPOINT")!)
{
}
internal BrowserScriptMiddleware(string webSocketUrl)
{
_scriptBytes = GetWebSocketClientJavaScript(webSocketUrl);
_contentLength = _scriptBytes.Length.ToString(CultureInfo.InvariantCulture);
}
public async Task InvokeAsync(HttpContext context)
{
context.Response.Headers["Cache-Control"] = "no-store";
context.Response.Headers["Content-Length"] = _contentLength;
context.Response.Headers["Content-Type"] = "application/javascript; charset=utf-8";
await context.Response.Body.WriteAsync(_scriptBytes.AsMemory(), context.RequestAborted);
}
internal static byte[] GetWebSocketClientJavaScript(string hostString)
{
var jsFileName = "Microsoft.AspNetCore.Watch.BrowserRefresh.WebSocketScriptInjection.js";
using var reader = new StreamReader(typeof(WebSocketScriptInjection).Assembly.GetManifestResourceStream(jsFileName)!);
var script = reader.ReadToEnd().Replace("{{hostString}}", hostString);
return Encoding.UTF8.GetBytes(script);
}
}
}

View File

@ -2,6 +2,7 @@
// 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 System; using System;
using System.Globalization;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -22,6 +23,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
{ {
return app => return app =>
{ {
app.Map(WebSocketScriptInjection.WebSocketScriptUrl, app1 => app1.UseMiddleware<BrowserScriptMiddleware>());
app.UseMiddleware<BrowserRefreshMiddleware>(); app.UseMiddleware<BrowserRefreshMiddleware>();
next(app); next(app);
}; };

View File

@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
OnWrite(); OnWrite();
if (IsHtmlResponse && !ScriptInjectionPerformed) if (IsHtmlResponse && !ScriptInjectionPerformed)
{ {
ScriptInjectionPerformed = WebSocketScriptInjection.Instance.TryInjectLiveReloadScript(_baseStream, buffer); ScriptInjectionPerformed = WebSocketScriptInjection.TryInjectLiveReloadScript(_baseStream, buffer);
} }
else else
{ {
@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
if (IsHtmlResponse && !ScriptInjectionPerformed) if (IsHtmlResponse && !ScriptInjectionPerformed)
{ {
ScriptInjectionPerformed = WebSocketScriptInjection.Instance.TryInjectLiveReloadScript(_baseStream, buffer.AsSpan(offset, count)); ScriptInjectionPerformed = WebSocketScriptInjection.TryInjectLiveReloadScript(_baseStream, buffer.AsSpan(offset, count));
} }
else else
{ {
@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
if (IsHtmlResponse && !ScriptInjectionPerformed) if (IsHtmlResponse && !ScriptInjectionPerformed)
{ {
ScriptInjectionPerformed = await WebSocketScriptInjection.Instance.TryInjectLiveReloadScriptAsync(_baseStream, buffer.AsMemory(offset, count), cancellationToken); ScriptInjectionPerformed = await WebSocketScriptInjection.TryInjectLiveReloadScriptAsync(_baseStream, buffer.AsMemory(offset, count), cancellationToken);
} }
else else
{ {
@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
if (IsHtmlResponse && !ScriptInjectionPerformed) if (IsHtmlResponse && !ScriptInjectionPerformed)
{ {
ScriptInjectionPerformed = await WebSocketScriptInjection.Instance.TryInjectLiveReloadScriptAsync(_baseStream, buffer, cancellationToken); ScriptInjectionPerformed = await WebSocketScriptInjection.TryInjectLiveReloadScriptAsync(_baseStream, buffer, cancellationToken);
} }
else else
{ {

View File

@ -13,22 +13,18 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
/// Helper class that handles the HTML injection into /// Helper class that handles the HTML injection into
/// a string or byte array. /// a string or byte array.
/// </summary> /// </summary>
public class WebSocketScriptInjection public static class WebSocketScriptInjection
{ {
private const string BodyMarker = "</body>"; private const string BodyMarker = "</body>";
internal const string WebSocketScriptUrl = "/_framework/aspnetcore-browser-refresh.js";
private readonly byte[] _bodyBytes = Encoding.UTF8.GetBytes(BodyMarker); private static readonly byte[] _bodyBytes = Encoding.UTF8.GetBytes(BodyMarker);
private readonly byte[] _scriptInjectionBytes;
public static WebSocketScriptInjection Instance { get; } = new WebSocketScriptInjection( internal static string InjectedScript { get; } = $"<script src=\"{WebSocketScriptUrl}\"></script>";
GetWebSocketClientJavaScript(Environment.GetEnvironmentVariable("ASPNETCORE_AUTO_RELOAD_WS_ENDPOINT")));
public WebSocketScriptInjection(string clientScript) private static readonly byte[] _injectedScriptBytes = Encoding.UTF8.GetBytes(InjectedScript);
{
_scriptInjectionBytes = Encoding.UTF8.GetBytes(clientScript);
}
public bool TryInjectLiveReloadScript(Stream baseStream, ReadOnlySpan<byte> buffer) public static bool TryInjectLiveReloadScript(Stream baseStream, ReadOnlySpan<byte> buffer)
{ {
var index = buffer.LastIndexOf(_bodyBytes); var index = buffer.LastIndexOf(_bodyBytes);
if (index == -1) if (index == -1)
@ -44,14 +40,14 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
} }
// Write the injected script // Write the injected script
baseStream.Write(_scriptInjectionBytes); baseStream.Write(_injectedScriptBytes);
// Write the rest of the buffer/HTML doc // Write the rest of the buffer/HTML doc
baseStream.Write(buffer); baseStream.Write(buffer);
return true; return true;
} }
public async ValueTask<bool> TryInjectLiveReloadScriptAsync(Stream baseStream, ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) public static async ValueTask<bool> TryInjectLiveReloadScriptAsync(Stream baseStream, ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{ {
var index = buffer.Span.LastIndexOf(_bodyBytes); var index = buffer.Span.LastIndexOf(_bodyBytes);
if (index == -1) if (index == -1)
@ -67,20 +63,12 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
} }
// Write the injected script // Write the injected script
await baseStream.WriteAsync(_scriptInjectionBytes, cancellationToken); await baseStream.WriteAsync(_injectedScriptBytes, cancellationToken);
// Write the rest of the buffer/HTML doc // Write the rest of the buffer/HTML doc
await baseStream.WriteAsync(buffer, cancellationToken); await baseStream.WriteAsync(buffer, cancellationToken);
return true; return true;
} }
internal static string GetWebSocketClientJavaScript(string? hostString)
{
var jsFileName = "Microsoft.AspNetCore.Watch.BrowserRefresh.WebSocketScriptInjection.js";
using var reader = new StreamReader(typeof(WebSocketScriptInjection).Assembly.GetManifestResourceStream(jsFileName)!);
var script = reader.ReadToEnd().Replace("{{hostString}}", hostString);
return $"<script>{script}</script>";
}
} }
} }

View File

@ -0,0 +1,66 @@
// 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 System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Xunit;
namespace Microsoft.AspNetCore.Watch.BrowserRefresh
{
public class BrowserScriptMiddlewareTest
{
[Fact]
public async Task InvokeAsync_ReturnsScript()
{
// Arrange
var context = new DefaultHttpContext();
var stream = new MemoryStream();
context.Response.Body = stream;
var middleware = new BrowserScriptMiddleware("some-host");
// Act
await middleware.InvokeAsync(context);
// Assert
stream.Position = 0;
var script = Encoding.UTF8.GetString(stream.ToArray());
Assert.Contains("// dotnet-watch browser reload script", script);
Assert.Contains("'some-host'", script);
}
[Fact]
public async Task InvokeAsync_ConfiguresHeaders()
{
// Arrange
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
var middleware = new BrowserScriptMiddleware("some-host");
// Act
await middleware.InvokeAsync(context);
// Assert
var response = context.Response;
Assert.Collection(
response.Headers.OrderBy(h => h.Key),
kvp =>
{
Assert.Equal("Cache-Control", kvp.Key);
Assert.Equal("no-store", kvp.Value);
},
kvp =>
{
Assert.Equal("Content-Length", kvp.Key);
Assert.NotEmpty(kvp.Value);
},
kvp =>
{
Assert.Equal("Content-Type", kvp.Key);
Assert.Equal("application/javascript; charset=utf-8", kvp.Value);
});
}
}
}

View File

@ -18,6 +18,7 @@
We'll simply test against it as source. We'll simply test against it as source.
--> -->
<Compile Include="..\src\BrowserRefreshMiddleware.cs" LinkBase="src" /> <Compile Include="..\src\BrowserRefreshMiddleware.cs" LinkBase="src" />
<Compile Include="..\src\BrowserScriptMiddleware.cs" LinkBase="src" />
<Compile Include="..\src\ResponseStreamWrapper.cs" LinkBase="src" /> <Compile Include="..\src\ResponseStreamWrapper.cs" LinkBase="src" />
<Compile Include="..\src\WebSocketScriptInjection.cs" LinkBase="src" /> <Compile Include="..\src\WebSocketScriptInjection.cs" LinkBase="src" />
<EmbeddedResource Include="..\src\WebSocketScriptInjection.js" /> <EmbeddedResource Include="..\src\WebSocketScriptInjection.js" />

View File

@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh.Tests
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
Assert.Contains("dotnet-watch browser reload script", content); Assert.Contains(WebSocketScriptInjection.InjectedScript, content);
} }
[Fact] [Fact]
@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh.Tests
Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header."); Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header.");
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
Assert.Contains("dotnet-watch browser reload script", content); Assert.Contains(WebSocketScriptInjection.InjectedScript, content);
} }
[Fact] [Fact]
@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh.Tests
Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header."); Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header.");
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
Assert.Contains("dotnet-watch browser reload script", content); Assert.Contains(WebSocketScriptInjection.InjectedScript, content);
} }
[Fact] [Fact]
@ -98,7 +98,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh.Tests
Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header."); Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header.");
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
Assert.Contains("dotnet-watch browser reload script", content); Assert.Contains(WebSocketScriptInjection.InjectedScript, content);
} }
[Fact] [Fact]
@ -131,7 +131,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh.Tests
Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header."); Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header.");
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
Assert.Contains("dotnet-watch browser reload script", content); Assert.Contains(WebSocketScriptInjection.InjectedScript, content);
} }
[Fact] [Fact]
@ -164,7 +164,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh.Tests
Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header."); Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header.");
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
Assert.Contains("dotnet-watch browser reload script", content); Assert.Contains(WebSocketScriptInjection.InjectedScript, content);
} }
[Fact] [Fact]

View File

@ -11,9 +11,6 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
{ {
public class WebSockerScriptInjectionTest public class WebSockerScriptInjectionTest
{ {
private const string ClientScript = "<script><!--My cool script--></script>";
private readonly WebSocketScriptInjection ScriptInjection = new WebSocketScriptInjection(ClientScript);
[Fact] [Fact]
public async Task TryInjectLiveReloadScriptAsync_DoesNotInjectMarkup_IfInputDoesNotContainBodyTag() public async Task TryInjectLiveReloadScriptAsync_DoesNotInjectMarkup_IfInputDoesNotContainBodyTag()
{ {
@ -22,7 +19,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
var input = Encoding.UTF8.GetBytes("<div>this is not a real body tag.</div>"); var input = Encoding.UTF8.GetBytes("<div>this is not a real body tag.</div>");
// Act // Act
var result = await ScriptInjection.TryInjectLiveReloadScriptAsync(stream, input); var result = await WebSocketScriptInjection.TryInjectLiveReloadScriptAsync(stream, input);
// Assert // Assert
Assert.False(result); Assert.False(result);
@ -37,7 +34,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
$@"<footer> $@"<footer>
This is the footer This is the footer
</footer> </footer>
{ClientScript}</body> {WebSocketScriptInjection.InjectedScript}</body>
</html>"; </html>";
var stream = new MemoryStream(); var stream = new MemoryStream();
var input = Encoding.UTF8.GetBytes( var input = Encoding.UTF8.GetBytes(
@ -48,7 +45,7 @@ $@"<footer>
</html>"); </html>");
// Act // Act
var result = await ScriptInjection.TryInjectLiveReloadScriptAsync(stream, input); var result = await WebSocketScriptInjection.TryInjectLiveReloadScriptAsync(stream, input);
// Assert // Assert
Assert.True(result); Assert.True(result);
@ -60,12 +57,12 @@ $@"<footer>
public async Task TryInjectLiveReloadScriptAsync_WithOffsetBodyTagAppearsInMiddle() public async Task TryInjectLiveReloadScriptAsync_WithOffsetBodyTagAppearsInMiddle()
{ {
// Arrange // Arrange
var expected = $"</table>{ClientScript}</body>"; var expected = $"</table>{WebSocketScriptInjection.InjectedScript}</body>";
var stream = new MemoryStream(); var stream = new MemoryStream();
var input = Encoding.UTF8.GetBytes("unused</table></body>"); var input = Encoding.UTF8.GetBytes("unused</table></body>");
// Act // Act
var result = await ScriptInjection.TryInjectLiveReloadScriptAsync(stream, input.AsMemory(6)); var result = await WebSocketScriptInjection.TryInjectLiveReloadScriptAsync(stream, input.AsMemory(6));
// Assert // Assert
Assert.True(result); Assert.True(result);
@ -77,12 +74,12 @@ $@"<footer>
public async Task TryInjectLiveReloadScriptAsync_WithOffsetBodyTagAppearsAtStartOfOffset() public async Task TryInjectLiveReloadScriptAsync_WithOffsetBodyTagAppearsAtStartOfOffset()
{ {
// Arrange // Arrange
var expected = $"{ClientScript}</body>"; var expected = $"{WebSocketScriptInjection.InjectedScript}</body>";
var stream = new MemoryStream(); var stream = new MemoryStream();
var input = Encoding.UTF8.GetBytes("unused</body>"); var input = Encoding.UTF8.GetBytes("unused</body>");
// Act // Act
var result = await ScriptInjection.TryInjectLiveReloadScriptAsync(stream, input.AsMemory(6)); var result = await WebSocketScriptInjection.TryInjectLiveReloadScriptAsync(stream, input.AsMemory(6));
// Assert // Assert
Assert.True(result); Assert.True(result);
@ -94,12 +91,12 @@ $@"<footer>
public async Task TryInjectLiveReloadScriptAsync_InjectsMarkupIfBodyTagAppearsAtTheStartOfOutput() public async Task TryInjectLiveReloadScriptAsync_InjectsMarkupIfBodyTagAppearsAtTheStartOfOutput()
{ {
// Arrange // Arrange
var expected = $"{ClientScript}</body></html>"; var expected = $"{WebSocketScriptInjection.InjectedScript}</body></html>";
var stream = new MemoryStream(); var stream = new MemoryStream();
var input = Encoding.UTF8.GetBytes("</body></html>"); var input = Encoding.UTF8.GetBytes("</body></html>");
// Act // Act
var result = await ScriptInjection.TryInjectLiveReloadScriptAsync(stream, input); var result = await WebSocketScriptInjection.TryInjectLiveReloadScriptAsync(stream, input);
// Assert // Assert
Assert.True(result); Assert.True(result);
@ -111,12 +108,12 @@ $@"<footer>
public async Task TryInjectLiveReloadScriptAsync_InjectsMarkupIfBodyTagAppearsByItself() public async Task TryInjectLiveReloadScriptAsync_InjectsMarkupIfBodyTagAppearsByItself()
{ {
// Arrange // Arrange
var expected = $"{ClientScript}</body>"; var expected = $"{WebSocketScriptInjection.InjectedScript}</body>";
var stream = new MemoryStream(); var stream = new MemoryStream();
var input = Encoding.UTF8.GetBytes("</body>"); var input = Encoding.UTF8.GetBytes("</body>");
// Act // Act
var result = await ScriptInjection.TryInjectLiveReloadScriptAsync(stream, input); var result = await WebSocketScriptInjection.TryInjectLiveReloadScriptAsync(stream, input);
// Assert // Assert
Assert.True(result); Assert.True(result);
@ -128,12 +125,12 @@ $@"<footer>
public async Task TryInjectLiveReloadScriptAsync_MultipleBodyTags() public async Task TryInjectLiveReloadScriptAsync_MultipleBodyTags()
{ {
// Arrange // Arrange
var expected = $"<p></body>some text</p>{ClientScript}</body>"; var expected = $"<p></body>some text</p>{WebSocketScriptInjection.InjectedScript}</body>";
var stream = new MemoryStream(); var stream = new MemoryStream();
var input = Encoding.UTF8.GetBytes("abc<p></body>some text</p></body>").AsMemory(3); var input = Encoding.UTF8.GetBytes("abc<p></body>some text</p></body>").AsMemory(3);
// Act // Act
var result = await ScriptInjection.TryInjectLiveReloadScriptAsync(stream, input); var result = await WebSocketScriptInjection.TryInjectLiveReloadScriptAsync(stream, input);
// Assert // Assert
Assert.True(result); Assert.True(result);
@ -150,7 +147,7 @@ $@"<footer>
var input = Encoding.UTF8.GetBytes(expected).AsSpan(); var input = Encoding.UTF8.GetBytes(expected).AsSpan();
// Act // Act
var result = ScriptInjection.TryInjectLiveReloadScript(stream, input); var result = WebSocketScriptInjection.TryInjectLiveReloadScript(stream, input);
// Assert // Assert
Assert.False(result); Assert.False(result);
@ -162,12 +159,12 @@ $@"<footer>
public void TryInjectLiveReloadScript_NoOffset() public void TryInjectLiveReloadScript_NoOffset()
{ {
// Arrange // Arrange
var expected = $"</table>{ClientScript}</body>"; var expected = $"</table>{WebSocketScriptInjection.InjectedScript}</body>";
var stream = new MemoryStream(); var stream = new MemoryStream();
var input = Encoding.UTF8.GetBytes("</table></body>").AsSpan(); var input = Encoding.UTF8.GetBytes("</table></body>").AsSpan();
// Act // Act
var result = ScriptInjection.TryInjectLiveReloadScript(stream, input); var result = WebSocketScriptInjection.TryInjectLiveReloadScript(stream, input);
// Assert // Assert
Assert.True(result); Assert.True(result);
@ -179,28 +176,17 @@ $@"<footer>
public void TryInjectLiveReloadScript_WithOffset() public void TryInjectLiveReloadScript_WithOffset()
{ {
// Arrange // Arrange
var expected = $"</table>{ClientScript}</body>"; var expected = $"</table>{WebSocketScriptInjection.InjectedScript}</body>";
var stream = new MemoryStream(); var stream = new MemoryStream();
var input = Encoding.UTF8.GetBytes("unused</table></body>").AsSpan(6); var input = Encoding.UTF8.GetBytes("unused</table></body>").AsSpan(6);
// Act // Act
var result = ScriptInjection.TryInjectLiveReloadScript(stream, input); var result = WebSocketScriptInjection.TryInjectLiveReloadScript(stream, input);
// Assert // Assert
Assert.True(result); Assert.True(result);
var output = Encoding.UTF8.GetString(stream.ToArray()); var output = Encoding.UTF8.GetString(stream.ToArray());
Assert.Equal(expected, output); Assert.Equal(expected, output);
} }
[Fact]
public void GetWebSocketClientJavaScript_Works()
{
// Act
var script = WebSocketScriptInjection.GetWebSocketClientJavaScript("some-host");
// Assert
Assert.Contains("// dotnet-watch browser reload script", script);
Assert.Contains("'some-host'", script);
}
} }
} }