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
Original file line number Diff line number Diff line change
Expand Up @@ -402,15 +402,19 @@ private static string NormalizeSubPath(string? subPath) {
string searchPattern = fileName;
string? directory = baseDirectory;
while (!string.IsNullOrEmpty(directory)) {
string candidate = Path.Combine(directory, searchPattern);
string candidate = Path.Join(directory, searchPattern);
if (Directory.Exists(candidate)) return candidate;

string[] children = [];
try {
children = Directory.GetDirectories(directory, searchPattern, SearchOption.TopDirectoryOnly);
}
catch (IOException) {}
catch (UnauthorizedAccessException) {}
catch (IOException) {
// Ignore
}
catch (UnauthorizedAccessException) {
// Ignore
}

if (children.Length > 0) return children[0];

Expand Down
2 changes: 1 addition & 1 deletion src/InfiniFrame.Js/TypeScript/Contracts/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ declare global {
// noinspection JSUnusedGlobalSymbols
interface Window {
infiniframe: InfiniFrame;
__dispatchMessageCallback?: (message: string) => void;
__infiniframe_dispatch?: (message: string) => void;

// Managed by the host: Webview or WebKit
chrome?: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,63 @@ describe("NativeInteropBridge", () => {

expect(existingReceive).toHaveBeenCalled();
});

it("defines __infiniframe_dispatch on window for webkit path", () => {
const alreadyDefined = "__infiniframe_dispatch" in window;
window.webkit = {messageHandlers: {infiniFrameInterop: {postMessage: vi.fn()}}} as any;

installNativeInteropBridge(setup);
if (!alreadyDefined) {
window.infiniframe.host!.receiveCallback(vi.fn());
}

expect((window as any).__infiniframe_dispatch).toBeDefined();
expect(typeof (window as any).__infiniframe_dispatch).toBe("function");
});

it("does NOT define __dispatchMessageCallback on window for webkit path", () => {
const alreadyDefined = "__infiniframe_dispatch" in window;
window.webkit = {messageHandlers: {infiniFrameInterop: {postMessage: vi.fn()}}} as any;

installNativeInteropBridge(setup);
if (!alreadyDefined) {
window.infiniframe.host!.receiveCallback(vi.fn());
}

expect((window as any).__dispatchMessageCallback).toBeUndefined();
});

it("dispatches messages to registered callbacks via __infiniframe_dispatch", () => {
window.webkit = {messageHandlers: {infiniFrameInterop: {postMessage: vi.fn()}}} as any;

installNativeInteropBridge(setup);
const cb = vi.fn();
try {
window.infiniframe.host!.receiveCallback(cb);
} catch {
// __infiniframe_dispatch is non-configurable; re-register may throw on re-defineProperty.
// The callback is still added to receiveCallbacks before the error.
}

(window as any).__infiniframe_dispatch("test-message");

expect(cb).toHaveBeenCalledWith("test-message");
});

it("dispatches to multiple callbacks via __infiniframe_dispatch", () => {
window.webkit = {messageHandlers: {infiniFrameInterop: {postMessage: vi.fn()}}} as any;

installNativeInteropBridge(setup);
const cb1 = vi.fn();
const cb2 = vi.fn();
try { window.infiniframe.host!.receiveCallback(cb1); } catch { /* non-configurable property */ }
try { window.infiniframe.host!.receiveCallback(cb2); } catch { /* non-configurable property */ }

(window as any).__infiniframe_dispatch("multi-callback-message");

expect(cb1).toHaveBeenCalledWith("multi-callback-message");
expect(cb2).toHaveBeenCalledWith("multi-callback-message");
});
});

describe("getDataAsync", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ void InfiniFrameWindow::SendWebMessage(const char* message) {
std::string escaped = escapeJsonString(message ? message : "");

std::string js;
js.append("__dispatchMessageCallback(\"");
js.append("__infiniframe_dispatch(\"");
js.append(escaped);
js.append("\")");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@
substringFromIndex: 1
];

NSString *javaScriptToEval = [NSString stringWithFormat: @"__dispatchMessageCallback(%@)", nsmessageJson];
NSString *javaScriptToEval = [NSString stringWithFormat: @"__infiniframe_dispatch(%@)", nsmessageJson];
return std::string([javaScriptToEval UTF8String]);
}
}
Expand Down Expand Up @@ -274,7 +274,7 @@
substringFromIndex: 1
];

NSString *javaScriptToEval = [NSString stringWithFormat: @"__dispatchMessageCallback(%@)", nsmessageJson];
NSString *javaScriptToEval = [NSString stringWithFormat: @"__infiniframe_dispatch(%@)", nsmessageJson];
[m_impl->_webview evaluateJavaScript: javaScriptToEval completionHandler: nil];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ IValidator<InfiniFrameNativeParameters> validator
) : ILifecycleInfiniFrameWindowFeature, IDisposable {
private static readonly InfiniFrameNative.ContextAction ReadyCallback = OnNativeReady;
private static readonly InfiniFrameNative.ContextAction TeardownCallback = OnNativeTeardown;
private readonly object _closeAttemptLock = new();
private readonly TaskCompletionSource _closed = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly TaskCompletionSource _closedCallbacksDelivered = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly TaskCompletionSource _messageLoopCompleted = new(TaskCreationOptions.RunContinuationsAsynchronously);
Expand All @@ -36,12 +35,23 @@ IValidator<InfiniFrameNativeParameters> validator
private GCHandle _milestoneRoot;
private int _milestoneRootReleased;
private int _nativeCallbackRootReleased;
public InfiniFrameWindowLifecycleState State => window.LifecycleState;

#if NET9_0_OR_GREATER
/// <summary>Synchronization lock for thread-safe access to pending task state.</summary>
private readonly Lock _closeAttemptLock = new();
#else
/// <summary>Synchronization lock for thread-safe access to pending task state.</summary>
private readonly object _closeAttemptLock = new();
#endif

// -----------------------------------------------------------------------------------------------------------------
// Methods
// -----------------------------------------------------------------------------------------------------------------
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
public InfiniFrameWindowLifecycleState State => window.LifecycleState;

/// <inheritdoc cref="ILifecycleInfiniFrameWindowFeature.CleanupNativeHandle" />
void ILifecycleInfiniFrameWindowFeature.CleanupNativeHandle() {
Expand Down Expand Up @@ -289,6 +299,8 @@ public async ValueTask CloseAsync(CancellationToken ct = default) {
&& Environment.CurrentManagedThreadId == window.ManagedThreadId
&& Volatile.Read(ref _messageLoopStarted) == 0
&& !IsClosed()) {
// ReSharper disable once MethodHasAsyncOverloadWithCancellation
// Yes this is needed to be synchronous else everything fails
WaitForClose();
}

Expand Down Expand Up @@ -332,9 +344,6 @@ void ILifecycleInfiniFrameWindowFeature.MarkCloseRejected() {
/// <inheritdoc cref="ILifecycleInfiniFrameWindowFeature.IsClosedOrClosing" />
public bool IsClosedOrClosing() => window.LifecycleState >= InfiniFrameWindowLifecycleState.ClosingRequested;

// -----------------------------------------------------------------------------------------------------------------
// Methods
// -----------------------------------------------------------------------------------------------------------------
/// <summary>
/// Provides the lifecycle management features for an InfiniFrame window.
/// Implements both <see cref="ILifecycleInfiniFrameWindowFeature" /> and <see cref="IDisposable" /> to handle
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// ---------------------------------------------------------------------------------------------------------------------
// Imports
// ---------------------------------------------------------------------------------------------------------------------
namespace InfiniTests.InfiniFrame.NativeBridge;
// ---------------------------------------------------------------------------------------------------------------------
// Code
// ---------------------------------------------------------------------------------------------------------------------
public class NativeJsDispatchNameContractTests {
private const string ExpectedDispatchName = "__infiniframe_dispatch";
private const string StaleDispatchName = "__dispatchMessageCallback";

// -----------------------------------------------------------------------------------------------------------------
// Test Methods
// -----------------------------------------------------------------------------------------------------------------
private static string FindRepoRoot() {
string? directory = AppContext.BaseDirectory;
while (directory != null) {
if (File.Exists(Path.Combine(directory, "InfiniFrame.slnx")))
return directory;
directory = Path.GetDirectoryName(directory);
}
throw new DirectoryNotFoundException("Could not locate the repository root containing InfiniFrame.slnx.");
}

private static async Task<string> ReadNativeSourceFile(string relativePath) {
string root = FindRepoRoot();
string fullPath = Path.Join(root, "src", relativePath);
await Assert.That(File.Exists(fullPath)).IsTrue();
return await File.ReadAllTextAsync(fullPath);
}

[Test]
public async Task Gtk_DispatchName_MatchesExpected(CancellationToken ct) {
string source = await ReadNativeSourceFile(
"InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowState.Gtk.cpp");

await Assert.That(source).Contains(ExpectedDispatchName);
await Assert.That(source).DoesNotContain(StaleDispatchName);
}

[Test]
public async Task Cocoa_BuildMacWebMessageJs_DispatchName_MatchesExpected(CancellationToken ct) {
string source = await ReadNativeSourceFile(
"InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/WindowState.Cocoa.mm");

await Assert.That(source).Contains(ExpectedDispatchName);
await Assert.That(source).DoesNotContain(StaleDispatchName);
}

[Test]
public async Task TypeScript_BridgeSource_DispatchName_MatchesExpected(CancellationToken ct) {
string root = FindRepoRoot();
string bridgePath = Path.Join(root, "src", "InfiniFrame.Js", "TypeScript", "Interop", "NativeInterop", "NativeInteropBridge.ts");
await Assert.That(File.Exists(bridgePath)).IsTrue();
string source = await File.ReadAllTextAsync(bridgePath, ct);

await Assert.That(source).Contains(ExpectedDispatchName);
await Assert.That(source).DoesNotContain(StaleDispatchName);
}

[Test]
public async Task TypeScript_GlobalDeclaration_DispatchName_MatchesExpected(CancellationToken ct) {
string root = FindRepoRoot();
string globalPath = Path.Join(root, "src", "InfiniFrame.Js", "TypeScript", "Contracts", "global.ts");
await Assert.That(File.Exists(globalPath)).IsTrue();
string source = await File.ReadAllTextAsync(globalPath, ct);

await Assert.That(source).Contains(ExpectedDispatchName);
await Assert.That(source).DoesNotContain(StaleDispatchName);
}
}
2 changes: 1 addition & 1 deletion tests/InfiniTests.InfiniFrame.SingleFile/CliTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public async Task DetectRid_KnownPlatforms_AllHaveValidRids(CancellationToken ct
public async Task Cli_ProjectArgument_IsRequired(CancellationToken ct = default) {
// The CLI requires a project argument - running without it should fail
string framework = Path.GetFileName(AppContext.BaseDirectory);
string cliPath = Path.GetFullPath(Path.Combine(
string cliPath = Path.GetFullPath(Path.Join(
AppContext.BaseDirectory,
"..", "..", "..", "..", "..", "..",
"src", "InfiniFrame.SingleFile", "bin", "Release", framework,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ public class SingleFileTargetsTests {
private static string GetRepoRoot() {
string? dir = AppContext.BaseDirectory;
while (dir is not null) {
if (Directory.Exists(Path.Combine(dir, ".git")))
if (Directory.Exists(Path.Join(dir, ".git")))
return dir;
dir = Path.GetDirectoryName(dir);
}
return Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", ".."));
return Path.GetFullPath(Path.Join(AppContext.BaseDirectory, "..", "..", "..", "..", "..", ".."));
}

private static string GetTargetsPath()
=> Path.Combine(GetRepoRoot(), "src", "InfiniFrame.SingleFile", "InfiniFrame.SingleFile.targets");
=> Path.Join(GetRepoRoot(), "src", "InfiniFrame.SingleFile", "InfiniFrame.SingleFile.targets");

[Test]
public async Task TargetsFile_Exists(CancellationToken ct = default) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ private static async Task<JsonDocument> GetGoldenVectorsAsync(CancellationToken
}

private static string ResolveGoldenVectorsPath() {
string outputLinkedPath = Path.Combine(AppContext.BaseDirectory, "Interop", GoldenVectorsFileName);
string outputLinkedPath = Path.Join(AppContext.BaseDirectory, "Interop", GoldenVectorsFileName);
if (File.Exists(outputLinkedPath))
return outputLinkedPath;

DirectoryInfo? current = new(AppContext.BaseDirectory);
while (current is not null) {
string candidate = Path.Combine(
string candidate = Path.Join(
current.FullName,
"src",
"InfiniFrame.Js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class FileProviderFactoryAdditionalTests {
public async Task CreateWwwrootProvider_WithExistingPhysicalPath_ReturnsDisposableComposite(CancellationToken ct = default) {
// Arrange
Assembly assembly = typeof(FileProviderFactory).Assembly;
string tempDir = Path.Combine(Path.GetTempPath(), $"InfiniFrameTest_{Guid.NewGuid():N}");
string tempDir = Path.Join(Path.GetTempPath(), $"InfiniFrameTest_{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
try {
// Act
Expand All @@ -36,7 +36,7 @@ public async Task CreateWwwrootProvider_WithExistingPhysicalPath_ReturnsDisposab
public async Task CreateWwwrootProvider_IncludePhysicalFallbackFalse_AlwaysReturnsComposite(CancellationToken ct = default) {
// Arrange
Assembly assembly = typeof(FileProviderFactory).Assembly;
string tempDir = Path.Combine(Path.GetTempPath(), $"InfiniFrameTest_{Guid.NewGuid():N}");
string tempDir = Path.Join(Path.GetTempPath(), $"InfiniFrameTest_{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
try {
// Act
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public async Task CreateWwwrootProvider_NullAssembly_UsesDefaultAssembly(Cancell
public async Task CreateWwwrootProvider_NonExistentPhysicalPath_ReturnsCompositeProvider(CancellationToken ct = default) {
// Arrange
Assembly assembly = typeof(FileProviderFactory).Assembly;
string nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "wwwroot");
string nonExistentPath = Path.Join(Path.GetTempPath(), Guid.NewGuid().ToString(), "wwwroot");

// Act
IFileProvider provider = FileProviderFactory.CreateWwwrootProvider(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public async Task AtWindowStage_FixedRuntimePath_StartsTheConfiguredFixedVersion
}

string runtimePath = await GetOrProvisionFixedRuntimePath(ct);
await Assert.That(File.Exists(Path.Combine(runtimePath, "msedgewebview2.exe"))).IsTrue();
await Assert.That(File.Exists(Path.Join(runtimePath, "msedgewebview2.exe"))).IsTrue();

int port = GetAvailableLoopbackPort();
using InfiniFrameTestWindow windowUtility = CreateWindowWithFixedRuntime(runtimePath, port, ct);
Expand All @@ -73,7 +73,7 @@ public async Task AtWindowStage_FixedRuntimePath_StartsTheConfiguredFixedVersion

private static async Task<string> GetOrProvisionFixedRuntimePath(CancellationToken ct) {
string? configuredPath = Environment.GetEnvironmentVariable("INFINIFRAME_TEST_WEBVIEW2_RUNTIME_PATH");
if (!string.IsNullOrWhiteSpace(configuredPath) && File.Exists(Path.Combine(configuredPath, "msedgewebview2.exe"))) {
if (!string.IsNullOrWhiteSpace(configuredPath) && File.Exists(Path.Join(configuredPath, "msedgewebview2.exe"))) {
return configuredPath;
}

Expand Down Expand Up @@ -110,7 +110,7 @@ private static string RunProvisioningScript(string scriptPath) {
}

string runtimePath = standardOutput.Trim();
if (!File.Exists(Path.Combine(runtimePath, "msedgewebview2.exe"))) {
if (!File.Exists(Path.Join(runtimePath, "msedgewebview2.exe"))) {
throw new InvalidOperationException("WebView2 fixed runtime provisioning returned an invalid runtime path.");
}

Expand All @@ -124,7 +124,7 @@ private static string RunProvisioningScript(string scriptPath) {
private static string FindRepositoryFile(params string[] relativePath) {
foreach (string startPath in new[] { AppContext.BaseDirectory, Environment.CurrentDirectory }) {
for (DirectoryInfo? directory = new(startPath); directory is not null; directory = directory.Parent) {
string candidate = Path.Combine([directory.FullName, .. relativePath]);
string candidate = Path.Join([directory.FullName, .. relativePath]);
if (File.Exists(candidate)) return candidate;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ private static IntPtr GetClassIcon(IntPtr hwnd, int index)
private static string ResolveRepoAsset(params string[] parts) {
string path = AppContext.BaseDirectory;
for (int i = 0; i < 5; i++) {
path = Path.GetFullPath(Path.Combine(path, ".."));
path = Path.GetFullPath(Path.Join(path, ".."));
}

foreach (string part in parts) {
path = Path.Combine(path, part);
path = Path.Join(path, part);
}

return path;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public async Task LoadAsync_WithStringUrl_ReturnsNavigationResult(CancellationTo
[SkipOnLinux]
public async Task LoadAsync_WithStringPath_ReturnsNavigationResult(CancellationToken ct) {
// Arrange
string tempFilePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.html");
string tempFilePath = Path.Join(Path.GetTempPath(), $"{Guid.NewGuid():N}.html");
await File.WriteAllTextAsync(tempFilePath, "<html><body>async-file</body></html>", ct);
using var windowUtility = InfiniFrameTestWindow.Create(ct);
IInfiniFrameWindow window = windowUtility.Window;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public async Task AtWindowStage_ExtensionAssignment(CancellationToken ct) {
[SkipOnLinux]
public async Task AtWindowStage_DirectAssignment_FileUri(CancellationToken ct) {
// Arrange
string tempFilePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.html");
string tempFilePath = Path.Join(Path.GetTempPath(), $"{Guid.NewGuid():N}.html");
await File.WriteAllTextAsync(tempFilePath, "<html><body>file-uri</body></html>", ct);
using var windowUtility = InfiniFrameTestWindow.Create(ct);
IInfiniFrameWindow window = windowUtility.Window;
Expand Down
Loading
Loading