Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/ProjectTemplates/Shared/Project.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,54 @@ internal AspNetProcess StartPublishedProjectAsync(bool hasListeningUri = true, b
return new AspNetProcess(DevCert, Output, TemplatePublishDir, projectDll, environment, published: true, hasListeningUri: hasListeningUri, usePublishedAppHost: usePublishedAppHost);
}

internal (ProcessEx process, string listeningUri) ServePublishedStandaloneApp(ITestOutputHelper output)
{
var publishDir = Path.Combine(TemplatePublishDir, "wwwroot");

output.WriteLine("Running dotnet serve on published output...");
var command = DotNetMuxer.MuxerPathOrDefault();
string args;
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HELIX_DIR")))
{
args = "serve";
}
else
{
command = "dotnet-serve";
args = "--roll-forward LatestMajor";
}

var serveProcess = ProcessEx.Run(output, publishDir, command, args);
var listeningUri = ResolveListeningUrl(serveProcess);
return (serveProcess, listeningUri);

static string ResolveListeningUrl(ProcessEx process)
{
var buffer = new List<string>();
try
{
foreach (var line in process.OutputLinesAsEnumerable)
{
if (line != null)
{
buffer.Add(line);
if (line.Trim().Contains("https://", StringComparison.Ordinal) ||
line.Trim().Contains("http://", StringComparison.Ordinal))
{
return line.Trim();
}
}
}
}
catch (OperationCanceledException)
{
}

throw new InvalidOperationException(
$"Couldn't find listening url:\n{string.Join(Environment.NewLine, buffer.Append(process.Error))}");
}
}

internal async Task RunDotNetEfCreateMigrationAsync(string migrationName)
{
var args = $"--verbose --no-build migrations add {migrationName}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,19 @@ class DotnetWebWorkerClient {
}
}
});

const dotnetJsUrl = DotnetWebWorkerClient.#resolveDotnetJsUrl();
worker.postMessage({ type: 'init', dotnetJsUrl });
});
Comment thread
ilonatommy marked this conversation as resolved.
}

static #resolveDotnetJsUrl() {
// Resolve using the browser's import map (handles fingerprinted URLs in published apps).
// Workers don't inherit the page's import map, so we resolve on the main thread and pass the URL.
const dotnetJsUrl = new URL('_framework/dotnet.js', document.baseURI).href;
return import.meta.resolve?.(dotnetJsUrl) ?? dotnetJsUrl;
}

invoke(method, args) {
return new Promise((resolve, reject) => {
const id = ++this.#requestId;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

import { dotnet } from '../../_framework/dotnet.js'

let workerExports = null;
let startupError = null;

try {
const { getAssemblyExports, getConfig } = await dotnet.create();
const assemblyName = getConfig().mainAssemblyName;
workerExports = await getAssemblyExports(assemblyName);
self.postMessage({ type: "ready" });
} catch (err) {
startupError = err.message;
console.error("[Worker] Failed to initialize .NET:", err);
self.postMessage({ type: "ready", error: err.message });
async function initialize(dotnetJsUrl) {
try {
const { dotnet } = await import(dotnetJsUrl);
const { getAssemblyExports, getConfig } = await dotnet.create();
const assemblyName = getConfig().mainAssemblyName;
workerExports = await getAssemblyExports(assemblyName);
self.postMessage({ type: "ready" });
} catch (err) {
const errorMessage = err?.message ?? String(err);
startupError = errorMessage;
console.error("[Worker] Failed to initialize .NET:", err);
self.postMessage({ type: "ready", error: errorMessage });
}
Comment thread
ilonatommy marked this conversation as resolved.
}

self.addEventListener('message', async (e) => {
if (e.data.type === 'init') {
await initialize(e.data.dotnetJsUrl);
return;
}

const { method, args, requestId } = e.data;

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public async Task BlazorWasmStandaloneTemplate_Works(BrowserKind browserKind)
}

// Test the published project
var (serveProcess, listeningUri) = RunPublishedStandaloneBlazorProject(project);
var (serveProcess, listeningUri) = project.ServePublishedStandaloneApp(Output);
using (serveProcess)
{
await TestBasicInteractionInNewPageAsync(browserKind, listeningUri, appName);
Expand Down Expand Up @@ -71,7 +71,7 @@ public async Task BlazorWasmStandalonePwaTemplate_Works(BrowserKind browserKind)
// Test the published project
if (BrowserManager.IsAvailable(browserKind))
{
var (serveProcess, listeningUri) = RunPublishedStandaloneBlazorProject(project);
var (serveProcess, listeningUri) = project.ServePublishedStandaloneApp(Output);
await using var browser = await BrowserManager.GetBrowserInstance(browserKind, BrowserContextInfo);
Output.WriteLine($"Opening browser at {listeningUri}...");
var page = await browser.NewPageAsync();
Expand Down Expand Up @@ -160,51 +160,4 @@ public TemplateInstance(string name, params string[] arguments)
public string Name { get; }
public string[] Arguments { get; }
}

private (ProcessEx, string url) RunPublishedStandaloneBlazorProject(Project project)
{
var publishDir = Path.Combine(project.TemplatePublishDir, "wwwroot");

Output.WriteLine("Running dotnet serve on published output...");
var command = DotNetMuxer.MuxerPathOrDefault();
string args;
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HELIX_DIR")))
{
args = $"serve ";
}
else
{
command = "dotnet-serve";
args = "--roll-forward LatestMajor"; // dotnet-serve targets net5.0 by default
}

var serveProcess = ProcessEx.Run(TestOutputHelper, publishDir, command, args);
var listeningUri = ResolveListeningUrl(serveProcess);
return (serveProcess, listeningUri);

static string ResolveListeningUrl(ProcessEx process)
{
var buffer = new List<string>();
try
{
foreach (var line in process.OutputLinesAsEnumerable)
{
if (line != null)
{
buffer.Add(line);
if (line.Trim().Contains("https://", StringComparison.Ordinal) || line.Trim().Contains("http://", StringComparison.Ordinal))
{
return line.Trim();
}
}
}
}
catch (OperationCanceledException)
{
}

throw new InvalidOperationException(
$"Couldn't find listening url:\n{string.Join(Environment.NewLine, buffer.Append(process.Error))}");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,37 @@ public async Task WebWorkerTemplate_CanDisposeWorker(BrowserKind browserKind)
await TestWebWorkerDisposal(browserKind, aspNetProcess.ListeningUri.AbsoluteUri + "webworker-test");
}

[Theory]
[InlineData(BrowserKind.Chromium)]
public async Task WebWorkerTemplate_CanInvokeMethodsAfterPublish(BrowserKind browserKind)
{
await using var testRun = await SetupWorkerLibAndPublish(_sharedHostProject);

var (serveProcess, listeningUri) = _sharedHostProject.ServePublishedStandaloneApp(Output);
using (serveProcess)
{
await TestWebWorkerInteraction(browserKind, listeningUri, clientRoute: "webworker-test");
}
}

private async Task<WorkerLibTestRun> SetupWorkerLibAndPublish(Project hostProject)
{
var parentDir = Path.GetDirectoryName(hostProject.TemplateOutputDir);
var workerLibDir = Path.Combine(parentDir, "WorkerLib");

if (Directory.Exists(workerLibDir))
{
Directory.Delete(workerLibDir, recursive: true);
}
Directory.CreateDirectory(workerLibDir);

await CreateWebWorkerLibrary(workerLibDir);
await AddWorkerLibReferenceAsync(hostProject);
await hostProject.RunDotNetPublishAsync(noRestore: false);

return new WorkerLibTestRun(workerLibDir, hostProject, Output);
}

private async Task<WorkerLibTestRun> SetupWorkerLibAndBuild(Project hostProject)
{
var parentDir = Path.GetDirectoryName(hostProject.TemplateOutputDir);
Expand Down Expand Up @@ -200,7 +231,7 @@ private void CopyTestAssets(Project hostProject)
File.Copy(workerMethodsSource, Path.Combine(hostProject.TemplateOutputDir, "TestWorkerMethods.cs"), overwrite: true);
}

private async Task TestWebWorkerInteraction(BrowserKind browserKind, string baseUri)
private async Task TestWebWorkerInteraction(BrowserKind browserKind, string baseUri, string clientRoute = null)
{
if (!BrowserManager.IsAvailable(browserKind))
{
Expand All @@ -211,7 +242,13 @@ private async Task TestWebWorkerInteraction(BrowserKind browserKind, string base
await using var browser = await BrowserManager.GetBrowserInstance(browserKind, BrowserContextInfo);
var page = await browser.NewPageAsync();

await page.GotoAsync(baseUri);
await page.GotoAsync(baseUri, new() { WaitUntil = WaitUntilState.NetworkIdle });
if (clientRoute != null)
{
// Static file servers (e.g., dotnet serve) don't support SPA fallback,
// so use Blazor's client-side navigation instead of a full page navigation.
await page.EvaluateAsync($"Blazor.navigateTo('{clientRoute}')");
}
await page.WaitForSelectorAsync("#webworker-test", new() { Timeout = 15000 });

await page.ClickAsync("#btn-init");
Expand Down
Loading