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 @@ -17,6 +17,7 @@ public static BunitContext AddFluentUI(this BunitContext ctx)
{
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.Services.AddFluentUIComponents();
ctx.Services.AddScoped<NoteBookmark.SharedUI.IUrlLauncher, NoteBookmark.SharedUI.JsUrlLauncher>();
return ctx;
}

Expand Down
1 change: 1 addition & 0 deletions src/NoteBookmark.BlazorApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();
builder.AddAzureTableClient("nb-tables");

Check warning on line 13 in src/NoteBookmark.BlazorApp/Program.cs

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'AspireTablesExtensions.AddAzureTableClient(IHostApplicationBuilder, string, Action<AzureDataTablesSettings>?, Action<IAzureClientBuilder<TableServiceClient, TableClientOptions>>?)' is obsolete: 'Use AddAzureTableServiceClient instead. This method will be removed in a future version.'

Check warning on line 13 in src/NoteBookmark.BlazorApp/Program.cs

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'AspireTablesExtensions.AddAzureTableClient(IHostApplicationBuilder, string, Action<AzureDataTablesSettings>?, Action<IAzureClientBuilder<TableServiceClient, TableClientOptions>>?)' is obsolete: 'Use AddAzureTableServiceClient instead. This method will be removed in a future version.'

// Add HTTP client for API calls
builder.Services.AddHttpClient<PostNoteClient>(client =>
Expand All @@ -19,6 +19,7 @@
});
builder.Services.AddTransient<IDataService>(sp => sp.GetRequiredService<PostNoteClient>());
builder.Services.AddSingleton<ILocalHtmlCache, NoteBookmark.BlazorApp.AlwaysAvailableHtmlCache>();
builder.Services.AddScoped<IUrlLauncher, JsUrlLauncher>();

// Register server-side AI settings provider (direct database access, unmasked)
builder.Services.AddScoped<AISettingsProvider>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<Compile Include="..\NoteBookmark.MauiApp\Data\ILocalHtmlStorageService.cs" Link="Data\ILocalHtmlStorageService.cs" />
<Compile Include="..\NoteBookmark.MauiApp\Data\SyncService.cs" Link="Data\SyncService.cs" />
<Compile Include="..\NoteBookmark.MauiApp\Data\SyncApiClient.cs" Link="Data\SyncApiClient.cs" />
<Compile Include="..\NoteBookmark.MauiApp\Data\MauiUrlLauncher.cs" Link="Data\MauiUrlLauncher.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
53 changes: 53 additions & 0 deletions src/NoteBookmark.MauiApp.Tests/UrlLauncherTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using FluentAssertions;
using Microsoft.JSInterop;
using Moq;
using NoteBookmark.MauiApp.Data;
using NoteBookmark.SharedUI;
using Xunit;

namespace NoteBookmark.MauiApp.Tests;

public class UrlLauncherTests
{
[Fact]
public async Task JsUrlLauncher_WithNullOrWhitespaceUrl_DoesNotInvokeJs()
{
var jsMock = new Mock<IJSRuntime>();
var launcher = new JsUrlLauncher(jsMock.Object);

await launcher.OpenUrlAsync(null);
await launcher.OpenUrlAsync(" ");

jsMock.VerifyNoOtherCalls();
}

[Fact]
public async Task JsUrlLauncher_WithValidUrl_InvokesWindowOpen()
{
var jsMock = new Mock<IJSRuntime>();
jsMock.Setup(x => x.InvokeAsync<object>("open", It.IsAny<object[]>()))
.ReturnsAsync(null!);

var launcher = new JsUrlLauncher(jsMock.Object);

await launcher.OpenUrlAsync("https://example.com");

jsMock.Verify(x => x.InvokeAsync<object>("open", It.Is<object[]>(args =>
args.Length == 2 && (string)args[0] == "https://example.com" && (string)args[1] == "_blank"
)), Times.Once);
}

[Fact]
public async Task MauiUrlLauncher_WithNullOrInvalidUrl_DoesNotThrow()
{
var launcher = new MauiUrlLauncher();

var act1 = async () => await launcher.OpenUrlAsync(null);
var act2 = async () => await launcher.OpenUrlAsync("not-a-valid-url");
var act3 = async () => await launcher.OpenUrlAsync("https://example.com");

await act1.Should().NotThrowAsync();
await act2.Should().NotThrowAsync();
await act3.Should().NotThrowAsync();
}
}
48 changes: 48 additions & 0 deletions src/NoteBookmark.MauiApp/Data/MauiUrlLauncher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#if !NOT_MAUI
using Microsoft.Maui.ApplicationModel.DataTransfer;
#endif
using NoteBookmark.SharedUI;

namespace NoteBookmark.MauiApp.Data;

public class MauiUrlLauncher : IUrlLauncher
{
#if !NOT_MAUI
private readonly IBrowser _browser;

public MauiUrlLauncher(IBrowser? browser = null)
{
_browser = browser ?? Browser.Default;
}
#else
public MauiUrlLauncher()
{
}
#endif

public async Task OpenUrlAsync(string? url)
{
if (string.IsNullOrWhiteSpace(url))
{
return;
}

if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
return;
}

try
{
#if !NOT_MAUI
await _browser.OpenAsync(uri, BrowserLaunchMode.SystemPreferred);
#else
await Task.CompletedTask;
#endif
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to launch URL in MAUI browser: {ex.Message}");
}
}
}
1 change: 1 addition & 0 deletions src/NoteBookmark.MauiApp/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static MauiHostingApp CreateMauiApp()
#endif

builder.Services.AddSingleton<Microsoft.Maui.Networking.IConnectivity>(Microsoft.Maui.Networking.Connectivity.Current);
builder.Services.AddSingleton<NoteBookmark.SharedUI.IUrlLauncher, NoteBookmark.MauiApp.Data.MauiUrlLauncher>();

// Data Layer
builder.Services.AddSingleton<NoteBookmark.MauiApp.Data.ILocalDataService, NoteBookmark.MauiApp.Data.LocalDataService>();
Expand Down
3 changes: 2 additions & 1 deletion src/NoteBookmark.SharedUI/Components/Pages/Posts.razor
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
@inject IDialogService DialogService
@inject NavigationManager Navigation
@inject ILocalHtmlCache localHtmlCache
@inject IUrlLauncher urlLauncher
@implements IDisposable

<PageTitle>Posts</PageTitle>
Expand Down Expand Up @@ -130,7 +131,7 @@

private async Task OpenUrlInNewWindow(string? url)
{
await jsRuntime.InvokeVoidAsync("open", url, "_blank");
await urlLauncher.OpenUrlAsync(url);
}

private async Task CreateNoteForPost(string postId)
Expand Down
3 changes: 2 additions & 1 deletion src/NoteBookmark.SharedUI/Components/Pages/Summaries.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@inject IDataService client
@inject NavigationManager Navigation
@inject IJSRuntime jsRuntime
@inject IUrlLauncher urlLauncher

<PageTitle>Summaries</PageTitle>

Expand Down Expand Up @@ -49,6 +50,6 @@

private async Task OpenUrlInNewWindow(string? url)
{
await jsRuntime.InvokeVoidAsync("open", url, "_blank");
await urlLauncher.OpenUrlAsync(url);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
@inject IJSRuntime jsRuntime
@inject IToastService toastService
@inject SummaryService aiService
@inject IUrlLauncher urlLauncher

<h2>SummaryEditor</h2>

Expand Down Expand Up @@ -138,7 +139,7 @@ else{

private async Task OpenUrlInNewWindow(string? url)
{
await jsRuntime.InvokeVoidAsync("open", url, "_blank");
await urlLauncher.OpenUrlAsync(url);
}

private void AddExtraNote(string category)
Expand Down Expand Up @@ -273,5 +274,4 @@ else{
isGenarating = false;
}
}

}
6 changes: 6 additions & 0 deletions src/NoteBookmark.SharedUI/IUrlLauncher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace NoteBookmark.SharedUI;

public interface IUrlLauncher
{
Task OpenUrlAsync(string? url);
}
30 changes: 30 additions & 0 deletions src/NoteBookmark.SharedUI/JsUrlLauncher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Microsoft.JSInterop;

namespace NoteBookmark.SharedUI;

public class JsUrlLauncher : IUrlLauncher
{
private readonly IJSRuntime _jsRuntime;

public JsUrlLauncher(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}

public async Task OpenUrlAsync(string? url)
{
if (string.IsNullOrWhiteSpace(url))
{
return;
}

try
{
await _jsRuntime.InvokeVoidAsync("open", url, "_blank");
}
catch
{
// Ignore JS interop exceptions when opening URL fails
}
}
}
Loading