Skip to content

Commit

Permalink
Feature: mermaid helpers for installation and doing file based genera…
Browse files Browse the repository at this point in the history
…tion (#1814)

* add a default factory for the playwright mermaid renderer

* add file handler overload for getdiagram

* add getdiagram text reader overload

* Create InstallationHelper.cs

* Create InstallationHelperTests.cs

* logic to write diagram to file

* more unit tests

* Update PlaywrightRendererTests.cs

* Update PlaywrightRendererTests.cs

* add PlaywrightBrowserTypeAndChannel combination model

* refactor to use PlaywrightBrowserTypeAndChannel arg
  • Loading branch information
dpvreony authored Aug 3, 2024
1 parent db0db12 commit b1712db
Show file tree
Hide file tree
Showing 9 changed files with 607 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Whipstaff.Mermaid.Playwright
{
/// <summary>
/// Response Model for <see cref="PlaywrightRenderer.GetDiagram"/>.
/// Response Model for PlaywrightRenderer GetDiagram calls.
/// </summary>
/// <param name="Svg">The diagram in SVG format.</param>
/// <param name="Png">PNG image of the diagram as a byte array.</param>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) 2022 DHGMS Solutions and Contributors. All rights reserved.
// This file is licensed to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;
using System.IO.Abstractions;
using System.Threading.Tasks;

namespace Whipstaff.Mermaid.Playwright
{
/// <summary>
/// Extension methods for <see cref="GetDiagramResponseModel"/>.
/// </summary>
public static class GetDiagramResponseModelExtensions
{
/// <summary>
/// Write the diagram to a file.
/// </summary>
/// <param name="diagramResponseModel">Diagram response model to save.</param>
/// <param name="targetFile">Target file to write to.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static Task ToFileAsync(
this GetDiagramResponseModel diagramResponseModel,
IFileInfo targetFile)
{
ArgumentNullException.ThrowIfNull(diagramResponseModel);
ArgumentNullException.ThrowIfNull(targetFile);
if (targetFile.Exists)
{
throw new ArgumentException("Target file already exists", nameof(targetFile));
}

return diagramResponseModel.InternalToFileAsync(
targetFile);
}

internal static async Task InternalToFileAsync(
this GetDiagramResponseModel diagramResponseModel,
IFileInfo targetFile)
{
using (var textWriter = targetFile.CreateText())
{
await textWriter.WriteAsync(diagramResponseModel.Svg)
.ConfigureAwait(false);
}
}
}
}
128 changes: 118 additions & 10 deletions src/Whipstaff.Mermaid/Playwright/PlaywrightRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
// See the LICENSE file in the project root for full license information.

using System;
using System.IO;
using System.IO.Abstractions;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Playwright;
using Whipstaff.Mermaid.HttpServer;
using Whipstaff.Playwright;
using Whipstaff.Runtime.Extensions;

namespace Whipstaff.Mermaid.Playwright
{
Expand All @@ -34,29 +38,133 @@ public PlaywrightRenderer(
_logMessageActionsWrapper = logMessageActionsWrapper;
}

/// <summary>
/// Create a default instance of the PlaywrightRenderer using the InMemory Test Http Server.
/// </summary>
/// <param name="loggerFactory">Logger factory instance to hook up to. Typically, the one being used by the host application.</param>
/// <returns>Instance of the <see cref="PlaywrightRenderer"/> class.</returns>
public static PlaywrightRenderer Default(ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(loggerFactory);

return new(
MermaidHttpServerFactory.GetTestServer(loggerFactory),
new PlaywrightRendererLogMessageActionsWrapper(
new PlaywrightRendererLogMessageActions(),
loggerFactory.CreateLogger<PlaywrightRenderer>()));
}

/// <summary>
/// Gets the SVG for the Mermaid Diagram from a File and writes to another file.
/// </summary>
/// <param name="sourceFile">File containing the diagram markdown to convert.</param>
/// <param name="targetFile">Destination file to write the diagram content to.</param>
/// <param name="playwrightBrowserTypeAndChannel">Browser and channel type to use.</param>
/// <returns>SVG diagram.</returns>
public async Task CreateDiagramAndWriteToFileAsync(
IFileInfo sourceFile,
IFileInfo targetFile,
PlaywrightBrowserTypeAndChannel playwrightBrowserTypeAndChannel)
{
ArgumentNullException.ThrowIfNull(sourceFile);
ArgumentNullException.ThrowIfNull(targetFile);

if (!sourceFile.Exists)
{
throw new ArgumentException("Source file does not exist", nameof(sourceFile));
}

if (sourceFile.FullName == targetFile.FullName)
{
throw new ArgumentException("Source and target files cannot be the same", nameof(targetFile));
}

if (targetFile.Exists)
{
throw new ArgumentException("Target file already exists", nameof(targetFile));
}

var diagram = await GetDiagram(
sourceFile,
playwrightBrowserTypeAndChannel)
.ConfigureAwait(false);

if (diagram == null)
{
throw new InvalidOperationException("Failed to get diagram");
}

await diagram.InternalToFileAsync(targetFile)
.ConfigureAwait(false);
}

/// <summary>
/// Gets the SVG for the Mermaid Diagram from a File.
/// </summary>
/// <param name="sourceFileInfo">File containing the diagram markdown to convert.</param>
/// <param name="playwrightBrowserTypeAndChannel">Browser and channel type to use.</param>
/// <returns>SVG diagram.</returns>
public async Task<GetDiagramResponseModel?> GetDiagram(
IFileInfo sourceFileInfo,
PlaywrightBrowserTypeAndChannel playwrightBrowserTypeAndChannel)
{
ArgumentNullException.ThrowIfNull(sourceFileInfo);

if (!sourceFileInfo.Exists)
{
throw new ArgumentException("File does not exist", nameof(sourceFileInfo));
}

using (var streamReader = sourceFileInfo.OpenText())
{
return await GetDiagram(
streamReader,
playwrightBrowserTypeAndChannel)
.ConfigureAwait(false);
}
}

/// <summary>
/// Gets the SVG for the Mermaid Diagram from a <see cref="TextReader"/>.
/// </summary>
/// <param name="textReader">File containing the diagram markdown to convert.</param>
/// <param name="playwrightBrowserTypeAndChannel">Browser and channel type to use.</param>
/// <returns>SVG diagram.</returns>
public async Task<GetDiagramResponseModel?> GetDiagram(
TextReader textReader,
PlaywrightBrowserTypeAndChannel playwrightBrowserTypeAndChannel)
{
ArgumentNullException.ThrowIfNull(textReader);

var markdown = await textReader
.ReadToEndAsync()
.ConfigureAwait(false);

return await GetDiagram(
markdown,
playwrightBrowserTypeAndChannel)
.ConfigureAwait(false);
}

/// <summary>
/// Gets the SVG for the Mermaid Diagram.
/// </summary>
/// <param name="markdown">Diagram markdown to convert.</param>
/// <param name="playwrightBrowserType">Browser type to use.</param>
/// <param name="browserChannel">The channel to use for the specified browser.</param>
/// <param name="playwrightBrowserTypeAndChannel">Browser and channel type to use.</param>
/// <returns>SVG diagram.</returns>
public async Task<GetDiagramResponseModel?> GetDiagram(
string markdown,
PlaywrightBrowserType playwrightBrowserType,
string? browserChannel)
PlaywrightBrowserTypeAndChannel playwrightBrowserTypeAndChannel)
{
if (string.IsNullOrWhiteSpace(markdown))
{
throw new ArgumentNullException(nameof(markdown));
}
markdown.ThrowIfNullOrWhitespace();
ArgumentNullException.ThrowIfNull(playwrightBrowserTypeAndChannel);

using (var playwright = await Microsoft.Playwright.Playwright.CreateAsync()
.ConfigureAwait(false))
await using (var browser = await playwright.GetBrowserType(playwrightBrowserType).LaunchAsync(new()
await using (var browser = await playwright.GetBrowserType(playwrightBrowserTypeAndChannel.PlaywrightBrowserType).LaunchAsync(new()
{
Headless = true,
Channel = browserChannel
Channel = playwrightBrowserTypeAndChannel.Channel
}))
{
var page = await browser.NewPageAsync()
Expand Down
1 change: 1 addition & 0 deletions src/Whipstaff.Mermaid/Whipstaff.Mermaid.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.7" />
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="21.0.26" />
</ItemGroup>

<ItemGroup>
Expand Down
49 changes: 49 additions & 0 deletions src/Whipstaff.Playwright/InstallationHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2022 DHGMS Solutions and Contributors. All rights reserved.
// This file is licensed to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;
using Microsoft.Playwright;

namespace Whipstaff.Playwright
{
/// <summary>
/// Helper class for installing Playwright browser. The out-of-the-box experience is a bit clunky.
///
/// This attempts to make it a bit easier. By allowing a first run execution to install the browser.
///
/// See:
/// https://github.com/microsoft/playwright-dotnet/issues/2239
/// https://github.com/microsoft/playwright-dotnet/issues/2286
///
/// This also removes the need to have msbuild run the task which has been causing CodeQL failures.
/// </summary>
public sealed class InstallationHelper
{
private readonly Lazy<int> _lazyInstaller;

/// <summary>
/// Initializes a new instance of the <see cref="InstallationHelper"/> class.
/// </summary>
/// <param name="playwrightBrowserType">The browser to use as part of the process.</param>
public InstallationHelper(PlaywrightBrowserType playwrightBrowserType)
{
var args = playwrightBrowserType switch
{
PlaywrightBrowserType.None => new[] { "install" },
_ => new[] { "install", playwrightBrowserType.ToString().ToLowerInvariant() },
};

_lazyInstaller = new(() => Program.Main(args));
}

/// <summary>
/// Wrapper to carry out an installation a single time.
/// </summary>
/// <returns>result code from Playwright install.</returns>
public int InstallPlaywright()
{
return _lazyInstaller.Value;
}
}
}
89 changes: 89 additions & 0 deletions src/Whipstaff.Playwright/PlaywrightBrowserTypeAndChannel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) 2022 DHGMS Solutions and Contributors. All rights reserved.
// This file is licensed to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Whipstaff.Runtime.Extensions;

namespace Whipstaff.Playwright
{
/// <summary>
/// Represents a Playwright browser type and channel combination.
/// </summary>
public sealed class PlaywrightBrowserTypeAndChannel
{
internal PlaywrightBrowserTypeAndChannel(PlaywrightBrowserType playwrightBrowserType, string? channel)
{
PlaywrightBrowserType = playwrightBrowserType;
Channel = channel;
}

/// <summary>
/// Gets the Playwright browser type.
/// </summary>
public PlaywrightBrowserType PlaywrightBrowserType { get; }

/// <summary>
/// Gets the browser channel, if any.
/// </summary>
public string? Channel { get; }

/// <summary>
/// Creates a new instance of the PlaywrightBrowserTypeAndChannel class for the default Playwright Chromium browser.
/// </summary>
/// <returns><see cref="PlaywrightBrowserTypeAndChannel"/> instance.</returns>
public static PlaywrightBrowserTypeAndChannel ChromiumDefault() => new(PlaywrightBrowserType.Chromium, null);

/// <summary>
/// Creates a new instance of the PlaywrightBrowserTypeAndChannel class for the Google Chrome Beta browser.
/// </summary>
/// <returns><see cref="PlaywrightBrowserTypeAndChannel"/> instance.</returns>
public static PlaywrightBrowserTypeAndChannel Chrome() => new(PlaywrightBrowserType.Chromium, "chrome");

/// <summary>
/// Creates a new instance of the PlaywrightBrowserTypeAndChannel class for the Google Chrome browser.
/// </summary>
/// <returns><see cref="PlaywrightBrowserTypeAndChannel"/> instance.</returns>
public static PlaywrightBrowserTypeAndChannel ChromeBeta() => new(PlaywrightBrowserType.Chromium, "chrome-beta");

/// <summary>
/// Creates a new instance of the PlaywrightBrowserTypeAndChannel class for a custom Chromium browser.
/// </summary>
/// <param name="channel">The custom channel to use.</param>
/// <returns><see cref="PlaywrightBrowserTypeAndChannel"/> instance.</returns>
public static PlaywrightBrowserTypeAndChannel ChromiumCustom(string channel)
{
channel.ThrowIfNullOrWhitespace();
return new PlaywrightBrowserTypeAndChannel(PlaywrightBrowserType.Chromium, channel);
}

/// <summary>
/// Creates a new instance of the PlaywrightBrowserTypeAndChannel class for the default Microsoft Edge browser.
/// </summary>
/// <returns><see cref="PlaywrightBrowserTypeAndChannel"/> instance.</returns>
public static PlaywrightBrowserTypeAndChannel MsEdge() => new(PlaywrightBrowserType.Chromium, "msedge");

/// <summary>
/// Creates a new instance of the PlaywrightBrowserTypeAndChannel class for the Microsoft Edge Beta browser.
/// </summary>
/// <returns><see cref="PlaywrightBrowserTypeAndChannel"/> instance.</returns>
public static PlaywrightBrowserTypeAndChannel MsEdgeBeta() => new(PlaywrightBrowserType.Chromium, "msedge-beta");

/// <summary>
/// Creates a new instance of the PlaywrightBrowserTypeAndChannel class for the Microsoft Edge Dev browser.
/// </summary>
/// <returns><see cref="PlaywrightBrowserTypeAndChannel"/> instance.</returns>
public static PlaywrightBrowserTypeAndChannel MsEdgeDev() => new(PlaywrightBrowserType.Chromium, "msedge-dev");

/// <summary>
/// Creates a new instance of the PlaywrightBrowserTypeAndChannel class for the default Playwright Firefox browser.
/// </summary>
/// <returns><see cref="PlaywrightBrowserTypeAndChannel"/> instance.</returns>
public static PlaywrightBrowserTypeAndChannel Firefox() => new(PlaywrightBrowserType.Firefox, null);

/// <summary>
/// Creates a new instance of the PlaywrightBrowserTypeAndChannel class for the default Playwright Webkit browser.
/// </summary>
/// <returns><see cref="PlaywrightBrowserTypeAndChannel"/> instance.</returns>
public static PlaywrightBrowserTypeAndChannel Webkit() => new(PlaywrightBrowserType.WebKit, null);
}
}
Loading

0 comments on commit b1712db

Please sign in to comment.