-
Notifications
You must be signed in to change notification settings - Fork 2k
[Android] BlazorWebView: Simplify Blazor startup scripts #35053
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c9fba38
ff86d5f
42d2bed
99fa1cc
ae0890b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,6 +20,67 @@ internal class WebKitWebViewClient : WebViewClient | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static readonly Uri AppOriginUri = new(AppOrigin); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Single startup script that: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // - Is idempotent (guarded by window.__BlazorStarting) so duplicate OnPageFinished calls are safe. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // - Sets up window.external.sendMessage/receiveMessage for the Blazor interop bridge. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // - Listens for the native 'capturePort' message that delivers the native WebMessagePort. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // - Calls Blazor.start() only after the native port is captured, ensuring window.external.sendMessage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // has a live port before Blazor sends any messages. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // - Sets window.__BlazorStarted after the Blazor.start() Promise resolves (used by test helpers as a readiness signal). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // - Logs startup failures without discarding the captured native port because Blazor may already be attached. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // - Dispatches native→JS messages (arriving via PostWebMessage) directly to window.external.__callback. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // - Validates message origin: only processes messages from the native PostWebMessage API | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // (event.source === null), skipping messages from subframes or other JS contexts. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private const string BlazorInitScript = """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking housekeeping: |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (function () { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (window.__BlazorStarting) { return 'false'; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.__BlazorStarting = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.external = window.external || {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.external.sendMessage = function (message) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (window.__nativePort) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.__nativePort.postMessage(message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.external.receiveMessage = function (callback) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.external.__callback = callback; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
kubaflo marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (window.__BlazorMessageHandler) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.removeEventListener('message', window.__BlazorMessageHandler, false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.__BlazorMessageHandler = function (event) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Only process messages from the native PostWebMessage API. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Native messages have event.source === null; messages from subframes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // or other JS contexts have event.source set to the sending window. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (event.source !== null) { return; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keeping this above the Without a source check on capture, any script or iframe in the page can call The rfh->PostMessageEvent(std::nullopt, source_origin, target_origin, std::move(message));The source frame token is hardcoded Only nit:
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Logic and Correctness (guard made more restrictive) / Cross-Platform Consistency — Previously the window Two things worth confirming before merge:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (event.data === 'capturePort') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (event.ports && event.ports[0] && !window.__nativePort) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.__nativePort = event.ports[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Promise.resolve().then(function () { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Blazor.start(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }).then(function () { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.__BlazorStarted = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, function (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Blazor.start() failed:', err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
kubaflo marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+53
to
+71
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (window.external.__callback) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.external.__callback(event.data); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.addEventListener('message', window.__BlazorMessageHandler, false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 'true'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly BlazorWebViewHandler? _webViewHandler; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public WebKitWebViewClient(BlazorWebViewHandler webViewHandler) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -155,69 +216,16 @@ private void RunBlazorStartupScripts(AWebView view) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _webViewHandler?.Logger.RunningBlazorStartupScripts(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Confirm Blazor hasn't already initialized | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| view.EvaluateJavascript(@" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (function() { return typeof(window.__BlazorStarted); })(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ", new JavaScriptValueCallback(blazorStarted => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| view.EvaluateJavascript(BlazorInitScript, new JavaScriptValueCallback(result => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (blazorStarted?.ToString() != "\"undefined\"") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The init script returns 'true' if it performed first-time setup, or | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 'false' if it was a no-op (duplicate OnPageFinished). Only create the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // native MessageChannel when setup actually ran. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (result?.ToString() == "\"true\"") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [moderate] Logic and Correctness — If |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Blazor has already started, we can just abort startup process | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Set up JS ports | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| view.EvaluateJavascript(@" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const channel = new MessageChannel(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var nativeJsPortOne = channel.port1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var nativeJsPortTwo = channel.port2; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.addEventListener('message', function (event) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (event.data != 'capturePort') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nativeJsPortOne.postMessage(event.data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else if (event.data == 'capturePort') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (event.ports[0] != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nativeJsPortTwo = event.ports[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _webViewHandler?.WebviewManager?.SetUpMessageChannel(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _webViewHandler?.Logger.BlazorStartupScriptsSubmitted(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
kubaflo marked this conversation as resolved.
Comment on lines
+224
to
228
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no
In each case The old guard set This is also the right home for the reset that's currently in the JS rejection handler: on these paths
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nativeJsPortOne.addEventListener('message', function (event) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nativeJsPortTwo.addEventListener('message', function (event) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // data from native code to JS | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (window.external.__callback) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.external.__callback(event.data); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nativeJsPortOne.start(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nativeJsPortTwo.start(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.external.sendMessage = function (message) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // data from JS to native code | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nativeJsPortTwo.postMessage(message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.external.receiveMessage = function (callback) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.external.__callback = callback; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ", new JavaScriptValueCallback(_ => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Set up Server ports | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _webViewHandler?.WebviewManager?.SetUpMessageChannel(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Start Blazor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| view.EvaluateJavascript(@" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Blazor.start(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.__BlazorStarted = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ", new JavaScriptValueCallback(_ => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Done; no more action required | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _webViewHandler?.Logger.BlazorStartupScriptsFinished(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,6 +56,9 @@ internal static partial class Log | |
| [LoggerMessage(EventId = 16, Level = LogLevel.Debug, Message = "Blazor startup scripts finished.")] | ||
| public static partial void BlazorStartupScriptsFinished(this ILogger logger); | ||
|
|
||
| [LoggerMessage(EventId = 40, Level = LogLevel.Debug, Message = "Blazor startup scripts submitted.")] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two diagnosability notes now that this event is in play:
More importantly,
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[minor] Complexity Reduction / diagnosability — Adding After this change Suggested fix: keep both signals — have the JS post a native message (or set a value the C# side polls once) on successful |
||
| public static partial void BlazorStartupScriptsSubmitted(this ILogger logger); | ||
|
Comment on lines
+59
to
+60
|
||
|
|
||
| [LoggerMessage(EventId = 17, Level = LogLevel.Debug, Message = "Creating WebKit WKWebView...")] | ||
| public static partial void CreatingWebKitWKWebView(this ILogger logger); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,235 @@ | ||||||||||||||
| using System; | ||||||||||||||
| using System.Collections.Generic; | ||||||||||||||
| using System.Linq; | ||||||||||||||
| using System.Reflection; | ||||||||||||||
| using System.Threading.Tasks; | ||||||||||||||
| using Microsoft.AspNetCore.Components.WebView.Maui; | ||||||||||||||
| using Microsoft.Extensions.DependencyInjection; | ||||||||||||||
| using Microsoft.Extensions.Logging; | ||||||||||||||
| using Microsoft.Maui.MauiBlazorWebView.DeviceTests.Components; | ||||||||||||||
| using Xunit; | ||||||||||||||
| #if ANDROID | ||||||||||||||
| using PlatformWebView = Android.Webkit.WebView; | ||||||||||||||
| #elif IOS || MACCATALYST | ||||||||||||||
| using PlatformWebView = WebKit.WKWebView; | ||||||||||||||
| #elif WINDOWS | ||||||||||||||
| using PlatformWebView = Microsoft.UI.Xaml.Controls.WebView2; | ||||||||||||||
| #else | ||||||||||||||
| using PlatformWebView = System.Object; | ||||||||||||||
| #endif | ||||||||||||||
|
|
||||||||||||||
| namespace Microsoft.Maui.MauiBlazorWebView.DeviceTests.Elements; | ||||||||||||||
|
|
||||||||||||||
| public partial class BlazorWebViewTests | ||||||||||||||
| { | ||||||||||||||
| Task RunBlazorStartupTest( | ||||||||||||||
| Func<PlatformWebView, Task> test, | ||||||||||||||
| TestLoggerProvider? testLoggerProvider = null, | ||||||||||||||
| Type? componentType = null, | ||||||||||||||
| string? indexHtml = null, | ||||||||||||||
| Func<PlatformWebView, Task>? waitForReady = null) | ||||||||||||||
| { | ||||||||||||||
| return RunBlazorStartupTest( | ||||||||||||||
| (_, platformWebView) => test(platformWebView), | ||||||||||||||
| testLoggerProvider, | ||||||||||||||
| componentType, | ||||||||||||||
| indexHtml, | ||||||||||||||
| waitForReady); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| async Task RunBlazorStartupTest( | ||||||||||||||
| Func<BlazorWebViewHandler, PlatformWebView, Task> test, | ||||||||||||||
| TestLoggerProvider? testLoggerProvider = null, | ||||||||||||||
| Type? componentType = null, | ||||||||||||||
| string? indexHtml = null, | ||||||||||||||
| Func<PlatformWebView, Task>? waitForReady = null) | ||||||||||||||
| { | ||||||||||||||
| EnsureHandlerCreated(additionalCreationActions: appBuilder => | ||||||||||||||
| { | ||||||||||||||
| appBuilder.Services.AddMauiBlazorWebView(); | ||||||||||||||
| if (testLoggerProvider is not null) | ||||||||||||||
| { | ||||||||||||||
| appBuilder.Services.AddLogging(logging => | ||||||||||||||
| { | ||||||||||||||
| logging.AddFilter("Microsoft.AspNetCore.Components.WebView", LogLevel.Trace); | ||||||||||||||
| logging.AddProvider(testLoggerProvider); | ||||||||||||||
| }); | ||||||||||||||
| } | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| var bwv = new BlazorWebViewWithCustomFiles | ||||||||||||||
| { | ||||||||||||||
| HostPage = "wwwroot/index.html", | ||||||||||||||
| CustomFiles = new Dictionary<string, string> | ||||||||||||||
| { | ||||||||||||||
| { "index.html", indexHtml ?? TestStaticFilesContents.DefaultMauiIndexHtmlContent }, | ||||||||||||||
| }, | ||||||||||||||
| }; | ||||||||||||||
| bwv.RootComponents.Add(new RootComponent { ComponentType = componentType ?? typeof(NoOpComponent), Selector = "#app", }); | ||||||||||||||
|
|
||||||||||||||
| await InvokeOnMainThreadAsync(async () => | ||||||||||||||
|
kubaflo marked this conversation as resolved.
|
||||||||||||||
| { | ||||||||||||||
| var bwvHandler = CreateHandler<BlazorWebViewHandler>(bwv); | ||||||||||||||
| var platformWebView = bwvHandler.PlatformView; | ||||||||||||||
| if (waitForReady is null) | ||||||||||||||
| { | ||||||||||||||
| await WebViewHelpers.WaitForWebViewReady(platformWebView); | ||||||||||||||
| await WebViewHelpers.WaitForControlDiv(platformWebView, controlValueToWaitFor: "Static"); | ||||||||||||||
| } | ||||||||||||||
| else | ||||||||||||||
| { | ||||||||||||||
| await waitForReady(platformWebView); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| await test(bwvHandler, platformWebView); | ||||||||||||||
| }); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| [Fact] | ||||||||||||||
| public Task BlazorStartupSetsUpWindowExternal() => RunBlazorStartupTest(async platformWebView => | ||||||||||||||
| { | ||||||||||||||
| // window.external should have sendMessage and receiveMessage functions on all platforms. | ||||||||||||||
| // Use .toString() to normalize boolean results across platforms (iOS returns "1" for true). | ||||||||||||||
| var hasSendMessage = await WebViewHelpers.ExecuteScriptAsync(platformWebView, "(typeof window.external.sendMessage === 'function').toString()"); | ||||||||||||||
| var hasReceiveMessage = await WebViewHelpers.ExecuteScriptAsync(platformWebView, "(typeof window.external.receiveMessage === 'function').toString()"); | ||||||||||||||
|
|
||||||||||||||
| Assert.True(hasSendMessage == "true" || hasSendMessage == "\"true\"", $"Expected sendMessage to be a function, got: {hasSendMessage}"); | ||||||||||||||
| Assert.True(hasReceiveMessage == "true" || hasReceiveMessage == "\"true\"", $"Expected receiveMessage to be a function, got: {hasReceiveMessage}"); | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| #if ANDROID | ||||||||||||||
| [Fact] | ||||||||||||||
| public Task BlazorStartupSetsStartingAndStartedFlags() => RunBlazorStartupTest(async platformWebView => | ||||||||||||||
| { | ||||||||||||||
| // After Blazor is fully started, both flags should be true | ||||||||||||||
| var startingValue = await WebViewHelpers.ExecuteScriptAsync(platformWebView, "window.__BlazorStarting === true"); | ||||||||||||||
| var startedValue = await WebViewHelpers.ExecuteScriptAsync(platformWebView, "window.__BlazorStarted === true"); | ||||||||||||||
|
|
||||||||||||||
| Assert.Equal("true", startingValue); | ||||||||||||||
| Assert.Equal("true", startedValue); | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| [Fact] | ||||||||||||||
| public Task BlazorStartupCapturesNativePort() => RunBlazorStartupTest(async platformWebView => | ||||||||||||||
| { | ||||||||||||||
| // The native port should have been captured during startup | ||||||||||||||
| var hasNativePort = await WebViewHelpers.ExecuteScriptAsync(platformWebView, "window.__nativePort !== null && window.__nativePort !== undefined"); | ||||||||||||||
|
|
||||||||||||||
| Assert.Equal("true", hasNativePort); | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| [Fact] | ||||||||||||||
| public Task BlazorStartupScriptIsIdempotent() | ||||||||||||||
| { | ||||||||||||||
| var testLoggerProvider = new TestLoggerProvider(); | ||||||||||||||
| return RunBlazorStartupTest(async (bwvHandler, platformWebView) => | ||||||||||||||
| { | ||||||||||||||
| var setupEventsBefore = testLoggerProvider.GetEvents().Count( | ||||||||||||||
| logEvent => logEvent.EventId.Id == 40 && logEvent.EventId.Name == "BlazorStartupScriptsSubmitted"); | ||||||||||||||
| Assert.Equal(1, setupEventsBefore); | ||||||||||||||
|
|
||||||||||||||
| // Store the original native port reference | ||||||||||||||
| await WebViewHelpers.ExecuteScriptAsync(platformWebView, "window.__originalNativePort = window.__nativePort"); | ||||||||||||||
|
|
||||||||||||||
| var webViewClientField = typeof(BlazorWebViewHandler) | ||||||||||||||
| .GetField("_webViewClient", BindingFlags.Instance | BindingFlags.NonPublic); | ||||||||||||||
| Assert.NotNull(webViewClientField); | ||||||||||||||
|
|
||||||||||||||
| var webViewClient = webViewClientField.GetValue(bwvHandler) as global::Android.Webkit.WebViewClient; | ||||||||||||||
| Assert.NotNull(webViewClient); | ||||||||||||||
|
|
||||||||||||||
| // Simulate a duplicate OnPageFinished callback so the production startup path runs again. | ||||||||||||||
| webViewClient.OnPageFinished(platformWebView, platformWebView.Url); | ||||||||||||||
| await WebViewHelpers.ExecuteScriptAsync(platformWebView, "true"); | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Regression Prevention / Async and Threading — The assertion that discriminates a guard regression is The companion assertion at line 147 does not discriminate either: Suggested consolidated fix: replace the fake barrier with a positive wait plus a negative window. E.g. wait for a signal that provably follows the re-run (have the test assert on the value the init script returns, or use |
||||||||||||||
|
|
||||||||||||||
| // Verify the native port is still the same reference and no second channel setup was submitted. | ||||||||||||||
| var portUnchanged = await WebViewHelpers.ExecuteScriptAsync(platformWebView, "window.__nativePort === window.__originalNativePort"); | ||||||||||||||
| Assert.Equal("true", portUnchanged); | ||||||||||||||
|
|
||||||||||||||
| var setupEventsAfter = testLoggerProvider.GetEvents().Count( | ||||||||||||||
| logEvent => logEvent.EventId.Id == 40 && logEvent.EventId.Name == "BlazorStartupScriptsSubmitted"); | ||||||||||||||
| Assert.Equal(setupEventsBefore, setupEventsAfter); | ||||||||||||||
| }, testLoggerProvider); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| [Fact] | ||||||||||||||
| public Task BlazorStartupRejectionPreservesLiveBridge() | ||||||||||||||
| { | ||||||||||||||
| var rejectingIndexHtml = TestStaticFilesContents.DefaultMauiIndexHtmlContent.Replace( | ||||||||||||||
| "</body>", | ||||||||||||||
| """ | ||||||||||||||
| <script> | ||||||||||||||
| (function () { | ||||||||||||||
| const originalStart = Blazor.start.bind(Blazor); | ||||||||||||||
| Blazor.start = function (options) { | ||||||||||||||
| return originalStart(options).then(function (value) { | ||||||||||||||
| window.__wrappedBlazorStartRejected = true; | ||||||||||||||
| throw new Error('Deliberate post-start rejection'); | ||||||||||||||
| }); | ||||||||||||||
| }; | ||||||||||||||
| })(); | ||||||||||||||
| </script> | ||||||||||||||
| </body> | ||||||||||||||
| """, | ||||||||||||||
| StringComparison.Ordinal); | ||||||||||||||
|
|
||||||||||||||
| return RunBlazorStartupTest( | ||||||||||||||
| async platformWebView => | ||||||||||||||
| { | ||||||||||||||
| var nativePortPresent = await WebViewHelpers.ExecuteScriptAsync( | ||||||||||||||
| platformWebView, | ||||||||||||||
| "window.__nativePort !== null && window.__nativePort !== undefined"); | ||||||||||||||
| Assert.Equal("true", nativePortPresent); | ||||||||||||||
|
|
||||||||||||||
| await WebViewHelpers.ExecuteScriptAsync( | ||||||||||||||
| platformWebView, | ||||||||||||||
| "document.getElementById('incrementButton').click()"); | ||||||||||||||
| await WebViewHelpers.WaitForControlDiv(platformWebView, controlValueToWaitFor: "1"); | ||||||||||||||
| }, | ||||||||||||||
| componentType: typeof(TestComponent1), | ||||||||||||||
| indexHtml: rejectingIndexHtml, | ||||||||||||||
| waitForReady: async platformWebView => | ||||||||||||||
| { | ||||||||||||||
| await WebViewHelpers.WaitForCondition(platformWebView, "window.__wrappedBlazorStartRejected === true"); | ||||||||||||||
| await WebViewHelpers.WaitForControlDiv(platformWebView, controlValueToWaitFor: "0"); | ||||||||||||||
| }); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| [Fact] | ||||||||||||||
| public Task BlazorMessageDispatchOnlyProcessesNativeSourceMessages() => RunBlazorStartupTest(async platformWebView => | ||||||||||||||
| { | ||||||||||||||
| // Verify the message listener is working by checking that the native bridge | ||||||||||||||
| // dispatched messages during startup (Blazor is running, which means | ||||||||||||||
| // null-source native messages were processed correctly). | ||||||||||||||
| var blazorRunning = await WebViewHelpers.ExecuteScriptAsync(platformWebView, "window.Blazor != null"); | ||||||||||||||
| Assert.Equal("true", blazorRunning); | ||||||||||||||
|
Comment on lines
+201
to
+205
|
||||||||||||||
|
|
||||||||||||||
| // Set up: a callback counter for native dispatch, and a separate test-only | ||||||||||||||
| // listener that gives us a positive signal when the message event fires. | ||||||||||||||
| await WebViewHelpers.ExecuteScriptAsync(platformWebView, @" | ||||||||||||||
| window.__nativeCallbackCount = 0; | ||||||||||||||
| window.__testMessageSeen = false; | ||||||||||||||
| window.external.receiveMessage(function(msg) { window.__nativeCallbackCount++; }); | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor, but a trap for whoever extends this test: Android's Harmless today since the test ends immediately and each test builds a fresh handler, but chaining is nearly free:
Suggested change
Separately: this proves JS-sourced messages aren't dispatched, but nothing here proves native-sourced ones are — the |
||||||||||||||
| window.addEventListener('message', function(event) { | ||||||||||||||
| if (event.data === 'test-from-js') { window.__testMessageSeen = true; } | ||||||||||||||
| }, false); | ||||||||||||||
| "); | ||||||||||||||
|
|
||||||||||||||
| // Send a message from JS using window.postMessage — these have event.source | ||||||||||||||
| // set to the current window (non-null), so they go through a different path | ||||||||||||||
| // than native PostWebMessage messages (which have null source). | ||||||||||||||
| await WebViewHelpers.ExecuteScriptAsync(platformWebView, "window.postMessage('test-from-js', '*')"); | ||||||||||||||
|
|
||||||||||||||
| // Wait for positive confirmation that the message event was processed. | ||||||||||||||
| // Our test listener sets __testMessageSeen when it sees the message, | ||||||||||||||
| // which proves the event loop has processed it and the production | ||||||||||||||
| // listener also had its chance to run. | ||||||||||||||
| await WebViewHelpers.WaitForCondition(platformWebView, "window.__testMessageSeen === true"); | ||||||||||||||
|
|
||||||||||||||
| // Now we can safely check: the callback count should still be zero because | ||||||||||||||
| // only native-sourced messages (null source) are dispatched to the callback. | ||||||||||||||
| var count = await WebViewHelpers.ExecuteScriptAsync(platformWebView, "window.__nativeCallbackCount"); | ||||||||||||||
| Assert.Equal("0", count); | ||||||||||||||
| }); | ||||||||||||||
| #endif | ||||||||||||||
| } | ||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.