From 12c7252fb435c75cc703e739293c3d74ff08b028 Mon Sep 17 00:00:00 2001 From: Mikhail Korolev Date: Sun, 8 Mar 2026 16:41:29 +0300 Subject: [PATCH] refactor: remove sync pipeline API, make entire render chain async MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove TemplatePipeline.Process() and TemplateExpander.Expand() sync methods. All rendering now goes through ProcessAsync/ExpandAsync, fixing ContentElement expansion error "Content element expansion requires async processing". Also eliminates 3-4x redundant template processing in render methods — each async render method now calls ProcessAsync once and passes the pre-processed template through the entire chain. Additional fixes: - Fix missing _defaultRenderOptions in async Render(SKBitmap) overloads - Fix ExprValue.ToString() culture sensitivity (InvariantCulture) - Add regression test for ContentElement through async pipeline - Update 38 files: production code, CLI, and 170+ test methods --- .../Commands/DebugLayoutCommand.cs | 28 +-- src/FlexRender.Core/Parsing/Ast/ExprValue.cs | 4 +- .../TemplateEngine/TemplateExpander.cs | 34 --- .../TemplateEngine/TemplatePipeline.cs | 24 -- .../ImageSharpRender.cs | 32 ++- .../Rendering/ImageSharpRenderingEngine.cs | 48 +--- .../Rendering/RenderingEngine.cs | 99 +++++--- .../Rendering/SkiaRenderer.cs | 214 ++++++++---------- src/FlexRender.Skia.Render/SkiaRender.cs | 31 +-- .../Rendering/SvgRenderingEngine.cs | 17 +- .../ImageSharpRenderingEngineTests.cs | 22 +- .../Configuration/ResourceLimitsTests.cs | 2 +- .../Expressions/ExpressionInConditionTests.cs | 20 +- .../Integration/AllFeaturesIntegrationTest.cs | 2 +- .../Integration/CanvasRtlPipelineTests.cs | 4 +- .../Integration/WbReceiptIntegrationTests.cs | 8 +- .../Layout/FlexLayoutBugTests.cs | 16 +- .../Rendering/RenderingIntegrationTests.cs | 4 +- .../Rendering/SkiaRendererRotationTests.cs | 58 ++--- .../Rendering/SkiaRendererTests.cs | 193 +++++++++++----- .../SkiaTextShaperIntegrationTests.cs | 8 +- .../Rendering/SvgQrRenderingTests.cs | 28 ++- .../Snapshots/NdcSnapshotTests.cs | 52 ++--- .../Snapshots/SeparatorSnapshotTests.cs | 8 +- .../Snapshots/SnapshotTestBase.cs | 10 +- .../Snapshots/SvgSnapshotTestBase.cs | 10 +- .../Snapshots/SvgSnapshotTests.cs | 32 +-- .../Snapshots/VisualSnapshotTests.cs | 164 +++++++------- .../Snapshots/WbReceiptSnapshotTests.cs | 4 +- .../Table/TableExpansionTests.cs | 56 ++--- .../TemplateEngine/ConditionOperatorTests.cs | 184 +++++++-------- .../ExprValueIntegrationTests.cs | 72 +++--- .../TemplateEngine/TableExpanderTests.cs | 68 +++--- .../TemplateExpanderBytesImageTests.cs | 32 +-- .../TemplateEngine/TemplateExpanderTests.cs | 140 ++++++------ .../TemplateEngine/TemplatePipelineTests.cs | 28 +-- .../VisualEffects/BoxShadowParsingTests.cs | 4 +- .../VisualEffects/OpacityTests.cs | 4 +- 38 files changed, 877 insertions(+), 887 deletions(-) diff --git a/src/FlexRender.Cli/Commands/DebugLayoutCommand.cs b/src/FlexRender.Cli/Commands/DebugLayoutCommand.cs index 720f9ce..a1acd43 100644 --- a/src/FlexRender.Cli/Commands/DebugLayoutCommand.cs +++ b/src/FlexRender.Cli/Commands/DebugLayoutCommand.cs @@ -63,7 +63,7 @@ public static Command Create() /// Whether to enable verbose output. /// Optional fonts directory. /// Exit code (0 for success, non-zero for failure). - private static Task Execute( + private static async Task Execute( FileInfo templateFile, FileInfo? dataFile, FileInfo? outputFile, @@ -74,21 +74,21 @@ private static Task Execute( if (!templateFile.Exists) { Console.Error.WriteLine($"Error: Template file not found: {templateFile.FullName}"); - return Task.FromResult(1); + return 1; } // Validate data file if specified if (dataFile is not null && !dataFile.Exists) { Console.Error.WriteLine($"Error: Data file not found: {dataFile.FullName}"); - return Task.FromResult(1); + return 1; } // Validate fonts directory if specified if (fontsDir is not null && !fontsDir.Exists) { Console.Error.WriteLine($"Error: Fonts directory not found: {fontsDir.FullName}"); - return Task.FromResult(1); + return 1; } try @@ -137,7 +137,7 @@ private static Task Execute( } // Compute layout using the same renderer as actual rendering - var root = skiaRender.ComputeLayout(template, templateData); + var root = await skiaRender.ComputeLayout(template, templateData); // Print registered fonts Console.WriteLine("Fonts:"); @@ -158,17 +158,17 @@ private static Task Execute( // Optionally render debug image if (outputFile is not null) { - RenderDebugImage(template, root, templateData, outputFile.FullName, skiaRender); + await RenderDebugImage(template, root, templateData, outputFile.FullName, skiaRender); Console.WriteLine(); Console.WriteLine($"Debug image: {outputFile.FullName}"); } - return Task.FromResult(0); + return 0; } catch (TemplateParseException ex) { Console.Error.WriteLine($"Template error: {ex.Message}"); - return Task.FromResult(1); + return 1; } catch (Exception ex) { @@ -177,7 +177,7 @@ private static Task Execute( { Console.Error.WriteLine(ex.StackTrace); } - return Task.FromResult(1); + return 1; } } @@ -281,20 +281,22 @@ private static string GetComputedExtra(LayoutNode node) /// The template data. /// The output file path. /// The SkiaRender instance with fonts already registered. - private static void RenderDebugImage( + private static async Task RenderDebugImage( Template template, LayoutNode root, ObjectValue data, string outputPath, FlexRender.Skia.SkiaRender skiaRender) { - var size = skiaRender.Measure(template, data); + var size = await skiaRender.Measure(template, data); using var bitmap = new SKBitmap((int)Math.Ceiling(size.Width), (int)Math.Ceiling(size.Height)); using var canvas = new SKCanvas(bitmap); - // Render template normally - skiaRender.Render(canvas, template, data); + // Render template to PNG and decode back to draw onto debug canvas + var pngBytes = await skiaRender.RenderToPng(template, data); + using var rendered = SKBitmap.Decode(pngBytes); + canvas.DrawBitmap(rendered, 0, 0); // Draw debug overlay DrawDebugOverlay(canvas, root, 0, 0, skiaRender.FontManager); diff --git a/src/FlexRender.Core/Parsing/Ast/ExprValue.cs b/src/FlexRender.Core/Parsing/Ast/ExprValue.cs index 4e7b591..ad0770d 100644 --- a/src/FlexRender.Core/Parsing/Ast/ExprValue.cs +++ b/src/FlexRender.Core/Parsing/Ast/ExprValue.cs @@ -245,7 +245,7 @@ public override string ToString() if (IsExpression) return $"Expr({RawValue})"; if (RawValue is not null) - return $"Raw({RawValue})={Value}"; - return $"{Value}"; + return string.Create(CultureInfo.InvariantCulture, $"Raw({RawValue})={Value}"); + return string.Create(CultureInfo.InvariantCulture, $"{Value}"); } } diff --git a/src/FlexRender.Core/TemplateEngine/TemplateExpander.cs b/src/FlexRender.Core/TemplateEngine/TemplateExpander.cs index 4db9072..c6b3741 100644 --- a/src/FlexRender.Core/TemplateEngine/TemplateExpander.cs +++ b/src/FlexRender.Core/TemplateEngine/TemplateExpander.cs @@ -77,40 +77,6 @@ public TemplateExpander(ResourceLimits limits, FilterRegistry filterRegistry, Cu _resourceLoaders = resourceLoaders; } - /// - /// Expands EachElement and IfElement instances into concrete elements based on data. - /// Returns a new Template with all control flow elements resolved. - /// - /// The template containing control flow elements. - /// The data for evaluating conditions and iterating arrays. - /// A new Template with expanded elements. - /// Thrown when template or data is null. - /// Thrown when maximum expansion depth is exceeded. - public Template Expand(Template template, ObjectValue data) - { - ArgumentNullException.ThrowIfNull(template); - ArgumentNullException.ThrowIfNull(data); - - var context = new TemplateContext(data); - var expandedElements = ExpandElements(template.Elements, context, 0, template); - - var result = new Template - { - Name = template.Name, - Version = template.Version, - Canvas = template.Canvas, - Elements = expandedElements - }; - - // Copy fonts - foreach (var font in template.Fonts) - { - result.Fonts[font.Key] = font.Value; - } - - return result; - } - /// /// Asynchronously expands EachElement and IfElement instances into concrete elements based on data. /// Returns a new Template with all control flow elements resolved. diff --git a/src/FlexRender.Core/TemplateEngine/TemplatePipeline.cs b/src/FlexRender.Core/TemplateEngine/TemplatePipeline.cs index 3bbd1d2..a035b6a 100644 --- a/src/FlexRender.Core/TemplateEngine/TemplatePipeline.cs +++ b/src/FlexRender.Core/TemplateEngine/TemplatePipeline.cs @@ -25,30 +25,6 @@ public TemplatePipeline(TemplateExpander expander, TemplateProcessor templatePro _templateProcessor = templateProcessor; } - /// - /// Processes a template through the full pipeline: Expand, Resolve, Materialize. - /// - /// The parsed template to process. - /// The data context for expression evaluation. - /// The expanded and resolved template with all expressions materialized. - /// Thrown when or is null. - public Template Process(Template template, ObjectValue data) - { - ArgumentNullException.ThrowIfNull(template); - ArgumentNullException.ThrowIfNull(data); - - // Phase 1: Expand control flow (#if, #each, table) - var expanded = _expander.Expand(template, data); - - // Phase 2: Resolve expressions in all ExprValue properties - ResolveAll(expanded, data); - - // Phase 3: Materialize resolved strings into typed values - MaterializeAll(expanded); - - return expanded; - } - /// /// Asynchronously processes a template through the full pipeline: Expand, Resolve, Materialize. /// Uses await for the expansion phase to support async content source resolution. diff --git a/src/FlexRender.ImageSharp.Render/ImageSharpRender.cs b/src/FlexRender.ImageSharp.Render/ImageSharpRender.cs index 382c9fb..aaf04e4 100644 --- a/src/FlexRender.ImageSharp.Render/ImageSharpRender.cs +++ b/src/FlexRender.ImageSharp.Render/ImageSharpRender.cs @@ -190,8 +190,7 @@ public async Task RenderToPng( try { - using var image = _engine.RenderToImage( - layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate, _contentParserRegistry); + using var image = _engine.RenderToImage(processedTemplate, imageCache); var encoder = new PngEncoder(); await image.SaveAsync(output, encoder, cancellationToken).ConfigureAwait(false); @@ -241,8 +240,7 @@ public async Task RenderToJpeg( try { - using var image = _engine.RenderToImage( - layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate, _contentParserRegistry); + using var image = _engine.RenderToImage(processedTemplate, imageCache); var encoder = new JpegEncoder { Quality = effectiveOptions.Quality }; await image.SaveAsync(output, encoder, cancellationToken).ConfigureAwait(false); @@ -291,8 +289,7 @@ public async Task RenderToBmp( try { - using var image = _engine.RenderToImage( - layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate, _contentParserRegistry); + using var image = _engine.RenderToImage(processedTemplate, imageCache); var encoder = new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel32 }; await image.SaveAsync(output, encoder, cancellationToken).ConfigureAwait(false); @@ -339,8 +336,7 @@ public async Task RenderToRaw( try { - using var image = _engine.RenderToImage( - layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate, _contentParserRegistry); + using var image = _engine.RenderToImage(processedTemplate, imageCache); // Write raw RGBA pixel data var pixelCount = checked(image.Width * image.Height); @@ -360,26 +356,23 @@ public async Task RenderToRaw( // ======================================================================== /// - /// Pre-loads all images referenced in the template using the configured resource loaders. - /// Also returns the processed template so callers can skip redundant expand+preprocess steps. + /// Expands and processes the template asynchronously, then pre-loads all images + /// referenced in it using the configured resource loaders. /// - /// The template to scan for image references. + /// The template to process and scan for image references. /// The data context for expression evaluation. /// Cancellation token for async operations. /// - /// A tuple of the processed template and an image cache. The processed template is non-null - /// when resource loaders are configured (since expand+preprocess was already performed). - /// The image cache maps URIs to pre-loaded images, or is null when no images were found. + /// A tuple of the fully processed template and an image cache. The processed template + /// is always non-null. The image cache maps URIs to pre-loaded images, or is null + /// when no images were found or no resource loaders are configured. /// Caller is responsible for disposing images via . /// - private async Task<(Template? processedTemplate, Dictionary>? imageCache)> PreloadImages( + private async Task<(Template processedTemplate, Dictionary>? imageCache)> PreloadImages( Template template, ObjectValue data, CancellationToken cancellationToken) { - if (_resourceLoaders.Count == 0) - return (null, null); - // Expand, resolve, and materialize template to resolve expressions in image src attributes var expander = _filterRegistry is not null ? new TemplateExpander(_limits, _filterRegistry, _contentParserRegistry, _resourceLoaders) @@ -391,6 +384,9 @@ public async Task RenderToRaw( var pipeline = new TemplatePipeline(expander, templateProcessor); var processedTemplate = await pipeline.ProcessAsync(template, data).ConfigureAwait(false); + if (_resourceLoaders.Count == 0) + return (processedTemplate, null); + var uris = ImageSharpRenderingEngine.CollectImageUris(processedTemplate); if (uris.Count == 0) return (processedTemplate, null); diff --git a/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpRenderingEngine.cs b/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpRenderingEngine.cs index 01851ba..ea1180c 100644 --- a/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpRenderingEngine.cs +++ b/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpRenderingEngine.cs @@ -6,7 +6,6 @@ using FlexRender.Parsing.Ast; using FlexRender.Providers; using FlexRender.Rendering; -using FlexRender.TemplateEngine; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; @@ -67,52 +66,25 @@ internal ImageSharpRenderingEngine( } /// - /// Renders a template to a new Image<Rgba32>. + /// Renders a pre-processed template to a new Image<Rgba32>. + /// The caller must run the template through TemplatePipeline.ProcessAsync + /// before invoking this method. /// - /// The template to render. - /// The data context for expression evaluation. - /// Optional filter registry. + /// + /// A fully expanded and processed template. Must have all expressions resolved + /// and content materialised via TemplatePipeline.ProcessAsync. + /// /// /// Optional pre-loaded image cache for HTTP and other async sources. /// When provided, images are resolved from the cache before falling back to /// inline loading (file and base64 only). /// - /// - /// Optional pre-processed template from image preloading. When provided, the - /// expand+preprocess steps are skipped to avoid redundant work. - /// - /// Optional content parser registry for custom content type parsing. /// A new image containing the rendered template. Caller owns disposal. internal Image RenderToImage( - Template template, - ObjectValue data, - FilterRegistry? filterRegistry = null, - IReadOnlyDictionary>? imageCache = null, - Template? preprocessedTemplate = null, - ContentParserRegistry? contentParserRegistry = null) + Template processedTemplate, + IReadOnlyDictionary>? imageCache = null) { - ArgumentNullException.ThrowIfNull(template); - ArgumentNullException.ThrowIfNull(data); - - Template processedTemplate; - - if (preprocessedTemplate is not null) - { - processedTemplate = preprocessedTemplate; - } - else - { - // Expand, resolve, and materialize template via the Core pipeline - var expander = filterRegistry is not null - ? new TemplateExpander(_limits, filterRegistry, contentParserRegistry, _resourceLoaders) - : new TemplateExpander(_limits, contentParserRegistry, _resourceLoaders); - var templateProcessor = filterRegistry is not null - ? new TemplateProcessor(_limits, filterRegistry) - : new TemplateProcessor(_limits); - - var pipeline = new TemplatePipeline(expander, templateProcessor); - processedTemplate = pipeline.Process(template, data); - } + ArgumentNullException.ThrowIfNull(processedTemplate); // Register fonts from the processed template (backend-specific) _preprocessor.RegisterFonts(processedTemplate); diff --git a/src/FlexRender.Skia.Render/Rendering/RenderingEngine.cs b/src/FlexRender.Skia.Render/Rendering/RenderingEngine.cs index 0c96458..8f7529e 100644 --- a/src/FlexRender.Skia.Render/Rendering/RenderingEngine.cs +++ b/src/FlexRender.Skia.Render/Rendering/RenderingEngine.cs @@ -91,29 +91,25 @@ internal RenderingEngine( } /// - /// Core canvas rendering logic. Accepts an optional pre-loaded image cache - /// so that async render paths can pass it through without storing mutable state - /// on the renderer instance (thread safety). + /// Core canvas rendering logic. Accepts a pre-processed template and computed layout node. + /// The caller is responsible for calling and + /// before invoking this method. /// /// The canvas to render to. - /// The template to render. - /// The data for variable substitution. + /// The already-processed template (after pipeline expansion). + /// The pre-computed root layout node. /// Optional offset for rendering position. /// Optional pre-loaded image cache. /// Per-call rendering options. internal void RenderToCanvas( SKCanvas canvas, - Template template, - ObjectValue data, + Template processedTemplate, + LayoutNode rootNode, SKPoint offset, IReadOnlyDictionary? imageCache, RenderOptions? renderOptions = null) { var effectiveRenderOptions = renderOptions ?? RenderOptions.Default; - var pipeline = ResolvePipeline(effectiveRenderOptions, template); - var processedTemplate = pipeline.Process(template, data); - _preprocessor.RegisterFonts(processedTemplate); - var rootNode = _layoutEngine.ComputeLayout(processedTemplate); // Save canvas state canvas.Save(); @@ -138,47 +134,42 @@ internal void RenderToCanvas( } /// - /// Core bitmap rendering logic. Accepts an optional pre-loaded image cache - /// so that async render paths can pass it through without storing mutable state - /// on the renderer instance (thread safety). + /// Core bitmap rendering logic. Accepts a pre-processed template and computed layout node. + /// The caller is responsible for calling and + /// before invoking this method. /// /// The bitmap to render to. - /// The template to render. - /// The data for variable substitution. + /// The already-processed template (after pipeline expansion). + /// The pre-computed root layout node. /// Optional offset for rendering position. /// Optional pre-loaded image cache. /// Per-call rendering options. internal void RenderToBitmapCore( SKBitmap bitmap, - Template template, - ObjectValue data, + Template processedTemplate, + LayoutNode rootNode, SKPoint offset, IReadOnlyDictionary? imageCache, RenderOptions? renderOptions = null) { - var rotationDegrees = RotationHelper.ParseRotation(template.Canvas.Rotate.Value); + var rotationDegrees = RotationHelper.ParseRotation(processedTemplate.Canvas.Rotate.Value); // If no rotation needed, render directly to bitmap if (!RotationHelper.HasRotation(rotationDegrees)) { using var canvas = new SKCanvas(bitmap); - RenderToCanvas(canvas, template, data, offset, imageCache, renderOptions); + RenderToCanvas(canvas, processedTemplate, rootNode, offset, imageCache, renderOptions); return; } // Render to temporary bitmap first, then rotate - var effectiveRenderOptions = renderOptions ?? RenderOptions.Default; - var pipeline = ResolvePipeline(effectiveRenderOptions, template); - var processedTemplate = pipeline.Process(template, data); - _preprocessor.RegisterFonts(processedTemplate); - var rootNode = _layoutEngine.ComputeLayout(processedTemplate); var originalWidth = (int)rootNode.Width; var originalHeight = (int)rootNode.Height; using var tempBitmap = new SKBitmap(originalWidth, originalHeight); using (var tempCanvas = new SKCanvas(tempBitmap)) { - RenderToCanvas(tempCanvas, template, data, offset, imageCache, renderOptions); + RenderToCanvas(tempCanvas, processedTemplate, rootNode, offset, imageCache, renderOptions); } // Rotate the bitmap @@ -673,6 +664,34 @@ internal async Task> PreloadImagesAsync( return cache; } + /// + /// Pre-loads all images from an already-processed template asynchronously using the image loader. + /// Unlike , this method skips the pipeline processing step + /// and works directly with the provided template. + /// + /// The already-processed template containing resolved image references. + /// Cancellation token. + /// A dictionary mapping image URIs to loaded bitmaps. + internal async Task> PreloadImagesFromProcessedAsync( + Template processedTemplate, + CancellationToken cancellationToken) + { + var uris = CollectImageUris(processedTemplate); + var cache = new Dictionary(uris.Count, StringComparer.Ordinal); + + foreach (var uri in uris) + { + cancellationToken.ThrowIfCancellationRequested(); + var bitmap = await _imageLoader!.Load(uri, cancellationToken).ConfigureAwait(false); + if (bitmap is not null) + { + cache[uri] = bitmap; + } + } + + return cache; + } + /// /// Sets the SVG content cache on the SVG provider if it implements . /// @@ -714,6 +733,34 @@ internal async Task> PreloadSvgContentAsync( return cache; } + /// + /// Pre-loads all SVG content from an already-processed template asynchronously using the resource loaders. + /// Unlike , this method skips the pipeline processing step + /// and works directly with the provided template. + /// + /// The already-processed template containing resolved SVG element references. + /// Cancellation token. + /// A dictionary mapping SVG source URIs to loaded and sanitized SVG content. + internal async Task> PreloadSvgContentFromProcessedAsync( + Template processedTemplate, + CancellationToken cancellationToken) + { + var uris = SvgContentLoader.CollectSvgUris(processedTemplate); + var cache = new Dictionary(uris.Count, StringComparer.Ordinal); + + foreach (var uri in uris) + { + cancellationToken.ThrowIfCancellationRequested(); + var content = await SvgContentLoader.LoadFromLoaders(_resourceLoaders, uri).ConfigureAwait(false); + if (content is not null) + { + cache[uri] = content; + } + } + + return cache; + } + /// /// Resolves the border-radius for an element, clamped to half the smaller dimension. /// diff --git a/src/FlexRender.Skia.Render/Rendering/SkiaRenderer.cs b/src/FlexRender.Skia.Render/Rendering/SkiaRenderer.cs index ee7b3a9..0c49113 100644 --- a/src/FlexRender.Skia.Render/Rendering/SkiaRenderer.cs +++ b/src/FlexRender.Skia.Render/Rendering/SkiaRenderer.cs @@ -144,40 +144,46 @@ public SkiaRenderer( public FontManager FontManager => _fontManager; /// - /// Computes the layout tree for a template with data. + /// Computes the layout tree for a template with data asynchronously. /// Uses the same layout engine configuration as rendering (including text measurement). /// /// The template to lay out. /// The data for variable substitution. /// The root layout node with computed positions and sizes. - public LayoutNode ComputeLayout(Template template, ObjectValue data) + /// Thrown when the renderer has been disposed. + /// Thrown when or is null. + public async Task ComputeLayoutAsync(Template template, ObjectValue data) { ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); ArgumentNullException.ThrowIfNull(template); ArgumentNullException.ThrowIfNull(data); - var processedTemplate = _pipeline.Process(template, data); + var processedTemplate = await _pipeline.ProcessAsync(template, data).ConfigureAwait(false); _preprocessor.RegisterFonts(processedTemplate); return _layoutEngine.ComputeLayout(processedTemplate); } /// - /// Measures the size required to render the template. + /// Measures the size required to render the template asynchronously. /// Takes into account canvas rotation which may swap width and height. /// /// The template to measure. /// The data for variable substitution. + /// Cancellation token for async operation. /// The required size after rotation is applied. - public SKSize Measure(Template template, ObjectValue data) + /// Thrown when the renderer has been disposed. + /// Thrown when or is null. + /// Thrown when the operation is cancelled. + public async Task MeasureAsync(Template template, ObjectValue data, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); ArgumentNullException.ThrowIfNull(template); ArgumentNullException.ThrowIfNull(data); + cancellationToken.ThrowIfCancellationRequested(); - var processedTemplate = _pipeline.Process(template, data); + var processedTemplate = await _pipeline.ProcessAsync(template, data).ConfigureAwait(false); _preprocessor.RegisterFonts(processedTemplate); - // Use LayoutEngine to compute accurate sizes var rootNode = _layoutEngine.ComputeLayout(processedTemplate); var width = rootNode.Width; @@ -194,70 +200,9 @@ public SKSize Measure(Template template, ObjectValue data) } /// - /// Renders the template to a canvas. - /// - /// The canvas to render to. - /// The template to render. - /// The data for variable substitution. - /// Optional offset for rendering position. - public void Render(SKCanvas canvas, Template template, ObjectValue data, SKPoint offset = default) - { - ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); - ArgumentNullException.ThrowIfNull(canvas); - ArgumentNullException.ThrowIfNull(template); - ArgumentNullException.ThrowIfNull(data); - - _renderingEngine.RenderToCanvas(canvas, template, data, offset, imageCache: null, _defaultRenderOptions); - } - - /// - /// Renders the template to a bitmap. - /// Applies canvas rotation after rendering if specified in template settings. - /// - /// The bitmap to render to. - /// The template to render. - /// The data for variable substitution. - /// Optional offset for rendering position. - public void Render(SKBitmap bitmap, Template template, ObjectValue data, SKPoint offset = default) - { - ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); - ArgumentNullException.ThrowIfNull(bitmap); - ArgumentNullException.ThrowIfNull(template); - ArgumentNullException.ThrowIfNull(data); - - _renderingEngine.RenderToBitmapCore(bitmap, template, data, offset, imageCache: null, _defaultRenderOptions); - } - - /// - /// Renders the template using typed data. - /// - /// The data type implementing ITemplateData. - /// The canvas to render to. - /// The template to render. - /// The typed data. - /// Optional offset for rendering position. - public void Render(SKCanvas canvas, Template template, T data, SKPoint offset = default) - where T : ITemplateData - { - Render(canvas, template, data.ToTemplateValue(), offset); - } - - /// - /// Renders the template using typed data to a bitmap. - /// - /// The data type implementing ITemplateData. - /// The bitmap to render to. - /// The template to render. - /// The typed data. - /// Optional offset for rendering position. - public void Render(SKBitmap bitmap, Template template, T data, SKPoint offset = default) - where T : ITemplateData - { - Render(bitmap, template, data.ToTemplateValue(), offset); - } - - /// - /// Renders a template to a new bitmap. + /// Renders a template to a new bitmap asynchronously. + /// Processes the template once through the async pipeline and pre-loads all images and SVG content + /// before rendering. /// /// The template to render. /// The data context for template expressions. @@ -275,21 +220,28 @@ public async Task Render( ArgumentNullException.ThrowIfNull(data); cancellationToken.ThrowIfCancellationRequested(); + var processedTemplate = await _pipeline.ProcessAsync(layoutTemplate, data).ConfigureAwait(false); + _preprocessor.RegisterFonts(processedTemplate); + var imageCache = _imageLoader is not null - ? await _renderingEngine.PreloadImagesAsync(layoutTemplate, data, cancellationToken).ConfigureAwait(false) + ? await _renderingEngine.PreloadImagesFromProcessedAsync(processedTemplate, cancellationToken).ConfigureAwait(false) : null; - var svgContentCache = await _renderingEngine.PreloadSvgContentAsync(layoutTemplate, data, cancellationToken).ConfigureAwait(false); + var svgContentCache = await _renderingEngine.PreloadSvgContentFromProcessedAsync(processedTemplate, cancellationToken).ConfigureAwait(false); _renderingEngine.SetSvgContentCache(svgContentCache); try { - // Measure returns the final size after rotation - var size = Measure(layoutTemplate, data); + var rootNode = _layoutEngine.ComputeLayout(processedTemplate); + var rotationDegrees = RotationHelper.ParseRotation(processedTemplate.Canvas.Rotate.Value); + var size = RotationHelper.SwapsDimensions(rotationDegrees) + ? new SKSize(rootNode.Height, rootNode.Width) + : new SKSize(rootNode.Width, rootNode.Height); + var bitmap = new SKBitmap((int)size.Width, (int)size.Height); try { - _renderingEngine.RenderToBitmapCore(bitmap, layoutTemplate, data, default, imageCache); + _renderingEngine.RenderToBitmapCore(bitmap, processedTemplate, rootNode, default, imageCache, _defaultRenderOptions); return bitmap; } catch @@ -311,6 +263,8 @@ public async Task Render( /// /// Renders a template to an existing bitmap asynchronously. + /// Processes the template once through the async pipeline and pre-loads all images and SVG content + /// before rendering. /// /// The target bitmap to render onto. /// The template to render. @@ -332,16 +286,20 @@ public async Task Render( ArgumentNullException.ThrowIfNull(data); cancellationToken.ThrowIfCancellationRequested(); + var processedTemplate = await _pipeline.ProcessAsync(layoutTemplate, data).ConfigureAwait(false); + _preprocessor.RegisterFonts(processedTemplate); + var imageCache = _imageLoader is not null - ? await _renderingEngine.PreloadImagesAsync(layoutTemplate, data, cancellationToken).ConfigureAwait(false) + ? await _renderingEngine.PreloadImagesFromProcessedAsync(processedTemplate, cancellationToken).ConfigureAwait(false) : null; - var svgContentCache = await _renderingEngine.PreloadSvgContentAsync(layoutTemplate, data, cancellationToken).ConfigureAwait(false); + var svgContentCache = await _renderingEngine.PreloadSvgContentFromProcessedAsync(processedTemplate, cancellationToken).ConfigureAwait(false); _renderingEngine.SetSvgContentCache(svgContentCache); try { - _renderingEngine.RenderToBitmapCore(bitmap, layoutTemplate, data, offset, imageCache); + var rootNode = _layoutEngine.ComputeLayout(processedTemplate); + _renderingEngine.RenderToBitmapCore(bitmap, processedTemplate, rootNode, offset, imageCache, _defaultRenderOptions); } finally { @@ -356,6 +314,8 @@ public async Task Render( /// /// Renders a template to a PNG stream. + /// Processes the template once through the async pipeline and pre-loads all images and SVG content + /// before rendering. /// /// The output stream to write PNG data to. /// The template to render. @@ -379,20 +339,26 @@ public async Task RenderToPng( ArgumentNullException.ThrowIfNull(data); cancellationToken.ThrowIfCancellationRequested(); + var processedTemplate = await _pipeline.ProcessAsync(layoutTemplate, data).ConfigureAwait(false); + _preprocessor.RegisterFonts(processedTemplate); + var imageCache = _imageLoader is not null - ? await _renderingEngine.PreloadImagesAsync(layoutTemplate, data, cancellationToken).ConfigureAwait(false) + ? await _renderingEngine.PreloadImagesFromProcessedAsync(processedTemplate, cancellationToken).ConfigureAwait(false) : null; - var svgContentCache = await _renderingEngine.PreloadSvgContentAsync(layoutTemplate, data, cancellationToken).ConfigureAwait(false); + var svgContentCache = await _renderingEngine.PreloadSvgContentFromProcessedAsync(processedTemplate, cancellationToken).ConfigureAwait(false); _renderingEngine.SetSvgContentCache(svgContentCache); try { - // Measure returns the final size after rotation - var size = Measure(layoutTemplate, data); - using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); + var rootNode = _layoutEngine.ComputeLayout(processedTemplate); + var rotationDegrees = RotationHelper.ParseRotation(processedTemplate.Canvas.Rotate.Value); + var size = RotationHelper.SwapsDimensions(rotationDegrees) + ? new SKSize(rootNode.Height, rootNode.Width) + : new SKSize(rootNode.Width, rootNode.Height); - _renderingEngine.RenderToBitmapCore(bitmap, layoutTemplate, data, default, imageCache, renderOptions); + using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); + _renderingEngine.RenderToBitmapCore(bitmap, processedTemplate, rootNode, default, imageCache, renderOptions); using var image = SKImage.FromBitmap(bitmap); using var encodedData = image.Encode(SKEncodedImageFormat.Png, compressionLevel); @@ -411,6 +377,8 @@ public async Task RenderToPng( /// /// Renders a template to a JPEG stream. + /// Processes the template once through the async pipeline and pre-loads all images and SVG content + /// before rendering. /// /// The output stream to write JPEG data to. /// The template to render. @@ -441,20 +409,26 @@ public async Task RenderToJpeg( cancellationToken.ThrowIfCancellationRequested(); + var processedTemplate = await _pipeline.ProcessAsync(layoutTemplate, data).ConfigureAwait(false); + _preprocessor.RegisterFonts(processedTemplate); + var imageCache = _imageLoader is not null - ? await _renderingEngine.PreloadImagesAsync(layoutTemplate, data, cancellationToken).ConfigureAwait(false) + ? await _renderingEngine.PreloadImagesFromProcessedAsync(processedTemplate, cancellationToken).ConfigureAwait(false) : null; - var svgContentCache = await _renderingEngine.PreloadSvgContentAsync(layoutTemplate, data, cancellationToken).ConfigureAwait(false); + var svgContentCache = await _renderingEngine.PreloadSvgContentFromProcessedAsync(processedTemplate, cancellationToken).ConfigureAwait(false); _renderingEngine.SetSvgContentCache(svgContentCache); try { - // Measure returns the final size after rotation - var size = Measure(layoutTemplate, data); - using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); + var rootNode = _layoutEngine.ComputeLayout(processedTemplate); + var rotationDegrees = RotationHelper.ParseRotation(processedTemplate.Canvas.Rotate.Value); + var size = RotationHelper.SwapsDimensions(rotationDegrees) + ? new SKSize(rootNode.Height, rootNode.Width) + : new SKSize(rootNode.Width, rootNode.Height); - _renderingEngine.RenderToBitmapCore(bitmap, layoutTemplate, data, default, imageCache, renderOptions); + using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); + _renderingEngine.RenderToBitmapCore(bitmap, processedTemplate, rootNode, default, imageCache, renderOptions); using var image = SKImage.FromBitmap(bitmap); using var encodedData = image.Encode(SKEncodedImageFormat.Jpeg, quality); @@ -473,6 +447,8 @@ public async Task RenderToJpeg( /// /// Renders a template to a BMP stream. + /// Processes the template once through the async pipeline and pre-loads all images and SVG content + /// before rendering. /// /// The output stream to write BMP data to. /// The template to render. @@ -496,20 +472,26 @@ public async Task RenderToBmp( ArgumentNullException.ThrowIfNull(data); cancellationToken.ThrowIfCancellationRequested(); + var processedTemplate = await _pipeline.ProcessAsync(layoutTemplate, data).ConfigureAwait(false); + _preprocessor.RegisterFonts(processedTemplate); + var imageCache = _imageLoader is not null - ? await _renderingEngine.PreloadImagesAsync(layoutTemplate, data, cancellationToken).ConfigureAwait(false) + ? await _renderingEngine.PreloadImagesFromProcessedAsync(processedTemplate, cancellationToken).ConfigureAwait(false) : null; - var svgContentCache = await _renderingEngine.PreloadSvgContentAsync(layoutTemplate, data, cancellationToken).ConfigureAwait(false); + var svgContentCache = await _renderingEngine.PreloadSvgContentFromProcessedAsync(processedTemplate, cancellationToken).ConfigureAwait(false); _renderingEngine.SetSvgContentCache(svgContentCache); try { - // Measure returns the final size after rotation - var size = Measure(layoutTemplate, data); - using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); + var rootNode = _layoutEngine.ComputeLayout(processedTemplate); + var rotationDegrees = RotationHelper.ParseRotation(processedTemplate.Canvas.Rotate.Value); + var size = RotationHelper.SwapsDimensions(rotationDegrees) + ? new SKSize(rootNode.Height, rootNode.Width) + : new SKSize(rootNode.Width, rootNode.Height); - _renderingEngine.RenderToBitmapCore(bitmap, layoutTemplate, data, default, imageCache, renderOptions); + using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); + _renderingEngine.RenderToBitmapCore(bitmap, processedTemplate, rootNode, default, imageCache, renderOptions); BmpEncoder.Encode(bitmap, output, colorMode); } @@ -526,6 +508,8 @@ public async Task RenderToBmp( /// /// Renders a template to raw pixel data in BGRA8888 format. + /// Processes the template once through the async pipeline and pre-loads all images and SVG content + /// before rendering. /// /// The output stream to write raw pixel data to. /// The template to render. @@ -547,20 +531,26 @@ public async Task RenderToRaw( ArgumentNullException.ThrowIfNull(data); cancellationToken.ThrowIfCancellationRequested(); + var processedTemplate = await _pipeline.ProcessAsync(layoutTemplate, data).ConfigureAwait(false); + _preprocessor.RegisterFonts(processedTemplate); + var imageCache = _imageLoader is not null - ? await _renderingEngine.PreloadImagesAsync(layoutTemplate, data, cancellationToken).ConfigureAwait(false) + ? await _renderingEngine.PreloadImagesFromProcessedAsync(processedTemplate, cancellationToken).ConfigureAwait(false) : null; - var svgContentCache = await _renderingEngine.PreloadSvgContentAsync(layoutTemplate, data, cancellationToken).ConfigureAwait(false); + var svgContentCache = await _renderingEngine.PreloadSvgContentFromProcessedAsync(processedTemplate, cancellationToken).ConfigureAwait(false); _renderingEngine.SetSvgContentCache(svgContentCache); try { - // Measure returns the final size after rotation - var size = Measure(layoutTemplate, data); - using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); + var rootNode = _layoutEngine.ComputeLayout(processedTemplate); + var rotationDegrees = RotationHelper.ParseRotation(processedTemplate.Canvas.Rotate.Value); + var size = RotationHelper.SwapsDimensions(rotationDegrees) + ? new SKSize(rootNode.Height, rootNode.Width) + : new SKSize(rootNode.Width, rootNode.Height); - _renderingEngine.RenderToBitmapCore(bitmap, layoutTemplate, data, default, imageCache, renderOptions); + using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); + _renderingEngine.RenderToBitmapCore(bitmap, processedTemplate, rootNode, default, imageCache, renderOptions); // Copy raw pixel bytes directly from the bitmap var pixels = bitmap.Bytes; @@ -577,24 +567,6 @@ public async Task RenderToRaw( } } - /// - /// Measures template size without rendering. - /// - /// The template to measure. - /// The data context for template expressions. - /// Cancellation token for async operation. - /// The size of the template in pixels. - /// Thrown when or is null. - /// Thrown when the operation is cancelled. - public Task Measure( - Template layoutTemplate, - ObjectValue data, - CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - return Task.FromResult(Measure(layoutTemplate, data)); - } - /// /// Disposes the renderer and releases resources. /// diff --git a/src/FlexRender.Skia.Render/SkiaRender.cs b/src/FlexRender.Skia.Render/SkiaRender.cs index e3ce86e..95de212 100644 --- a/src/FlexRender.Skia.Render/SkiaRender.cs +++ b/src/FlexRender.Skia.Render/SkiaRender.cs @@ -63,48 +63,33 @@ public sealed class SkiaRender : IFlexRender public FontManager FontManager => _renderer.FontManager; /// - /// Computes layout for a template without rendering. + /// Asynchronously computes layout for a template without rendering. /// Intended for diagnostic and debugging tools. /// /// The parsed template. /// The template data. - /// The root layout node. + /// A task that represents the asynchronous operation, containing the root layout node. /// /// This method is intended for debugging and testing only. For production rendering, /// use /// or one of the format-specific RenderTo* methods instead. /// - public LayoutNode ComputeLayout(Template template, ObjectValue data) => - _renderer.ComputeLayout(template, data); + public Task ComputeLayout(Template template, ObjectValue data) => + _renderer.ComputeLayoutAsync(template, data); /// - /// Measures the size required to render the template. + /// Asynchronously measures the size required to render the template. /// /// The parsed template. /// The template data. - /// The measured size in pixels. + /// A task that represents the asynchronous operation, containing the measured size in pixels. /// /// This method is intended for debugging and testing only. For production rendering, /// use /// or one of the format-specific RenderTo* methods instead. /// - public SKSize Measure(Template template, ObjectValue data) => - _renderer.Measure(template, data); - - /// - /// Renders the template to an existing canvas. - /// Intended for diagnostic and debugging tools. - /// - /// The canvas to render to. - /// The parsed template. - /// The template data. - /// - /// This method is intended for debugging and testing only. For production rendering, - /// use - /// or one of the format-specific RenderTo* methods instead. - /// - public void Render(SKCanvas canvas, Template template, ObjectValue data) => - _renderer.Render(canvas, template, data); + public Task Measure(Template template, ObjectValue data) => + _renderer.MeasureAsync(template, data); /// [Obsolete("Use RenderToBmp() with BmpOptions instead. This property will be removed in a future version.")] diff --git a/src/FlexRender.Svg.Render/Rendering/SvgRenderingEngine.cs b/src/FlexRender.Svg.Render/Rendering/SvgRenderingEngine.cs index 3db0775..a550d43 100644 --- a/src/FlexRender.Svg.Render/Rendering/SvgRenderingEngine.cs +++ b/src/FlexRender.Svg.Render/Rendering/SvgRenderingEngine.cs @@ -116,7 +116,7 @@ internal async Task RenderToSvgAsync(Template template, ObjectValue data try { - return RenderToSvg(template, data); + return RenderToSvgFromProcessed(processedTemplate); } finally { @@ -128,20 +128,17 @@ internal async Task RenderToSvgAsync(Template template, ObjectValue data } /// - /// Renders a template with data to SVG markup. + /// Renders a pre-processed template to SVG markup. + /// The template must already be processed through the pipeline by the caller. /// - /// The template to render. - /// The data for variable substitution. + /// The already-processed template to render. /// The SVG markup as a string. - internal string RenderToSvg(Template template, ObjectValue data) + internal string RenderToSvgFromProcessed(Template processedTemplate) { - ArgumentNullException.ThrowIfNull(template); - ArgumentNullException.ThrowIfNull(data); - - var processedTemplate = _pipeline.Process(template, data); + ArgumentNullException.ThrowIfNull(processedTemplate); // Build font family map and font face declarations from the template. - // Returned as local variables to ensure thread safety when RenderToSvg is called concurrently. + // Returned as local variables to ensure thread safety when RenderToSvgFromProcessed is called concurrently. var (fontFamilyMap, fontFaces) = BuildFontMap(processedTemplate); var rootNode = _layoutEngine.ComputeLayout(processedTemplate); diff --git a/tests/FlexRender.ImageSharp.Tests/Rendering/ImageSharpRenderingEngineTests.cs b/tests/FlexRender.ImageSharp.Tests/Rendering/ImageSharpRenderingEngineTests.cs index 045d96d..7dc450a 100644 --- a/tests/FlexRender.ImageSharp.Tests/Rendering/ImageSharpRenderingEngineTests.cs +++ b/tests/FlexRender.ImageSharp.Tests/Rendering/ImageSharpRenderingEngineTests.cs @@ -36,7 +36,7 @@ public void RenderToImage_SimpleBackground_ProducesColoredImage() Canvas = new CanvasSettings { Width = 100, Height = 50, Fixed = FixedDimension.Both, Background = "#ff0000" } }; - using var image = _engine.RenderToImage(template, new ObjectValue()); + using var image = _engine.RenderToImage(template); Assert.Equal(100, image.Width); Assert.Equal(50, image.Height); @@ -57,7 +57,7 @@ public void RenderToImage_TextElement_DrawsText() }; template.AddElement(new TextElement { Content = "Hello", Size = "16", Color = "#000000" }); - using var image = _engine.RenderToImage(template, new ObjectValue()); + using var image = _engine.RenderToImage(template); // Verify some pixels are non-white (text was drawn) var hasNonWhitePixel = false; @@ -90,7 +90,7 @@ public void RenderToImage_SeparatorElement_DrawsLine() }; template.AddElement(new SeparatorElement { Color = "#000000", Thickness = 2 }); - using var image = _engine.RenderToImage(template, new ObjectValue()); + using var image = _engine.RenderToImage(template); // Verify some pixels are non-white (separator was drawn) var hasNonWhitePixel = false; @@ -125,7 +125,7 @@ public void RenderToImage_NestedFlex_ProducesImage() flex.AddChild(new TextElement { Content = "Inside flex", Color = "#ffffff", Size = "14" }); template.AddElement(flex); - using var image = _engine.RenderToImage(template, new ObjectValue()); + using var image = _engine.RenderToImage(template); Assert.Equal(300, image.Width); Assert.Equal(100, image.Height); @@ -145,7 +145,7 @@ public void RenderToImage_DisplayNone_SkipsElement() Display = Display.None }); - using var image = _engine.RenderToImage(template, new ObjectValue()); + using var image = _engine.RenderToImage(template); // All pixels should be white since the element is hidden var allWhite = true; @@ -193,7 +193,7 @@ public void RenderToImage_TextWithRotation_DrawsRotatedText() }); template.AddElement(flex); - using var image = _engine.RenderToImage(template, new ObjectValue()); + using var image = _engine.RenderToImage(template); Assert.True(HasNonWhitePixel(image), "Expected rotated text to be drawn"); } @@ -221,7 +221,7 @@ public void RenderToImage_TextWithFlipRotation_DrawsText() }); template.AddElement(flex); - using var image = _engine.RenderToImage(template, new ObjectValue()); + using var image = _engine.RenderToImage(template); Assert.True(HasNonWhitePixel(image), "Expected flipped text to be drawn"); } @@ -249,7 +249,7 @@ public void RenderToImage_TextWithNumericRotation_DrawsText() }); template.AddElement(flex); - using var image = _engine.RenderToImage(template, new ObjectValue()); + using var image = _engine.RenderToImage(template); Assert.True(HasNonWhitePixel(image), "Expected angled text to be drawn"); } @@ -270,7 +270,7 @@ public void RenderToImage_TextWithNoneRotation_DrawsNormally() Rotate = "none" }); - using var image = _engine.RenderToImage(template, new ObjectValue()); + using var image = _engine.RenderToImage(template); Assert.True(HasNonWhitePixel(image), "Expected text to be drawn normally with 'none' rotation"); } @@ -289,7 +289,7 @@ public void RenderToImage_SeparatorWithRotation_DrawsRotated() Rotate = "45" }); - using var image = _engine.RenderToImage(template, new ObjectValue()); + using var image = _engine.RenderToImage(template); Assert.True(HasNonWhitePixel(image), "Expected rotated separator to be drawn"); } @@ -317,7 +317,7 @@ public void RenderToImage_TextWithNegativeRotation_DrawsText() }); template.AddElement(flex); - using var image = _engine.RenderToImage(template, new ObjectValue()); + using var image = _engine.RenderToImage(template); Assert.True(HasNonWhitePixel(image), "Expected text with negative rotation to be drawn"); } diff --git a/tests/FlexRender.Tests/Configuration/ResourceLimitsTests.cs b/tests/FlexRender.Tests/Configuration/ResourceLimitsTests.cs index d777dd9..3831a67 100644 --- a/tests/FlexRender.Tests/Configuration/ResourceLimitsTests.cs +++ b/tests/FlexRender.Tests/Configuration/ResourceLimitsTests.cs @@ -355,7 +355,7 @@ public async Task FullPipeline_WithCustomLimits_RespectsAllLimits() // Verify SkiaRenderer uses limits using var renderer = new SkiaRenderer(limits); - var size = await renderer.Measure(template, new ObjectValue(), TestContext.Current.CancellationToken); + var size = await renderer.MeasureAsync(template, new ObjectValue(), TestContext.Current.CancellationToken); Assert.True(size.Width > 0); } diff --git a/tests/FlexRender.Tests/Expressions/ExpressionInConditionTests.cs b/tests/FlexRender.Tests/Expressions/ExpressionInConditionTests.cs index 7255f8a..661d60f 100644 --- a/tests/FlexRender.Tests/Expressions/ExpressionInConditionTests.cs +++ b/tests/FlexRender.Tests/Expressions/ExpressionInConditionTests.cs @@ -30,7 +30,7 @@ private static Template CreateTemplate(params TemplateElement[] elements) } [Fact] - public void Expand_IfWithArithmeticCondition_EvaluatesExpression() + public async Task Expand_IfWithArithmeticCondition_EvaluatesExpression() { // condition: price * quantity, greaterThan: 100 var ifElem = new IfElement( @@ -50,7 +50,7 @@ public void Expand_IfWithArithmeticCondition_EvaluatesExpression() }; // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: 25 * 5 = 125 > 100 => true => "Free shipping!" Assert.Single(result.Elements); @@ -59,7 +59,7 @@ public void Expand_IfWithArithmeticCondition_EvaluatesExpression() } [Fact] - public void Expand_IfWithArithmeticCondition_FalseCase() + public async Task Expand_IfWithArithmeticCondition_FalseCase() { var ifElem = new IfElement( new List { new TextElement { Content = "Free shipping!" } }, @@ -78,7 +78,7 @@ public void Expand_IfWithArithmeticCondition_FalseCase() }; // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: 5 * 2 = 10 > 100 => false => "Shipping: $5" Assert.Single(result.Elements); @@ -87,7 +87,7 @@ public void Expand_IfWithArithmeticCondition_FalseCase() } [Fact] - public void Expand_IfWithSubtractionCondition_EvaluatesExpression() + public async Task Expand_IfWithSubtractionCondition_EvaluatesExpression() { var ifElem = new IfElement( new List { new TextElement { Content = "Positive balance" } }, @@ -106,7 +106,7 @@ public void Expand_IfWithSubtractionCondition_EvaluatesExpression() }; // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: 100 - 30 = 70 > 0 => true Assert.Single(result.Elements); @@ -115,7 +115,7 @@ public void Expand_IfWithSubtractionCondition_EvaluatesExpression() } [Fact] - public void Expand_IfWithTruthyExpressionCondition_EvaluatesArithmeticResult() + public async Task Expand_IfWithTruthyExpressionCondition_EvaluatesArithmeticResult() { // Truthy check on arithmetic expression: price * quantity // Non-zero number is truthy @@ -134,7 +134,7 @@ public void Expand_IfWithTruthyExpressionCondition_EvaluatesArithmeticResult() }; // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: 10 * 3 = 30, truthy => "Has value" Assert.Single(result.Elements); @@ -143,7 +143,7 @@ public void Expand_IfWithTruthyExpressionCondition_EvaluatesArithmeticResult() } [Fact] - public void Expand_IfWithNullCoalesceCondition_EvaluatesExpression() + public async Task Expand_IfWithNullCoalesceCondition_EvaluatesExpression() { var ifElem = new IfElement( new List { new TextElement { Content = "Has name" } }, @@ -160,7 +160,7 @@ public void Expand_IfWithNullCoalesceCondition_EvaluatesExpression() }; // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: name is null, nickname is "JohnD" => truthy => "Has name" Assert.Single(result.Elements); diff --git a/tests/FlexRender.Tests/Integration/AllFeaturesIntegrationTest.cs b/tests/FlexRender.Tests/Integration/AllFeaturesIntegrationTest.cs index 8dca9db..71e9d46 100644 --- a/tests/FlexRender.Tests/Integration/AllFeaturesIntegrationTest.cs +++ b/tests/FlexRender.Tests/Integration/AllFeaturesIntegrationTest.cs @@ -261,7 +261,7 @@ public async Task AllFeatures_MeasureLayout_ReturnsReasonableSize() var template = _parser.Parse(AllFeaturesYaml); var data = CreateTestData(hasDiscount: false, subtotal: 150); - var size = await _renderer.Measure(template, data, TestContext.Current.CancellationToken); + var size = await _renderer.MeasureAsync(template, data, TestContext.Current.CancellationToken); Assert.Equal(400f, size.Width); Assert.True(size.Height > 100, "Layout height should be substantial for a multi-section template"); diff --git a/tests/FlexRender.Tests/Integration/CanvasRtlPipelineTests.cs b/tests/FlexRender.Tests/Integration/CanvasRtlPipelineTests.cs index 0c54e82..dc579ff 100644 --- a/tests/FlexRender.Tests/Integration/CanvasRtlPipelineTests.cs +++ b/tests/FlexRender.Tests/Integration/CanvasRtlPipelineTests.cs @@ -31,7 +31,7 @@ public void Dispose() /// Canvas.TextDirection to the processed canvas, causing RTL mirroring to be lost. /// [Fact] - public void Parse_CanvasRtl_RowChildrenPositionedRightToLeft() + public async Task Parse_CanvasRtl_RowChildrenPositionedRightToLeft() { const string yaml = """ canvas: @@ -59,7 +59,7 @@ public void Parse_CanvasRtl_RowChildrenPositionedRightToLeft() var data = new ObjectValue(); // Full pipeline: expand -> preprocess -> layout - var root = _renderer.ComputeLayout(template, data); + var root = await _renderer.ComputeLayoutAsync(template, data); // root has one child: the row flex container Assert.Single(root.Children); diff --git a/tests/FlexRender.Tests/Integration/WbReceiptIntegrationTests.cs b/tests/FlexRender.Tests/Integration/WbReceiptIntegrationTests.cs index 8c92872..c2cfa3f 100644 --- a/tests/FlexRender.Tests/Integration/WbReceiptIntegrationTests.cs +++ b/tests/FlexRender.Tests/Integration/WbReceiptIntegrationTests.cs @@ -17,7 +17,7 @@ public void Dispose() } [Fact] - public void WbReceipt_RendersWithBlackColumns() + public async Task WbReceipt_RendersWithBlackColumns() { // Layout: 630px total width with 8px gaps between 3 columns // Left column: 130px black, Center: 450px white, Right: 26px black @@ -59,7 +59,7 @@ public void WbReceipt_RendersWithBlackColumns() var data = new ObjectValue(); using var bitmap = new SKBitmap(630, 200); - var exception = Record.Exception(() => _renderer.Render(bitmap, template, data)); + var exception = await Record.ExceptionAsync(async () => await _renderer.Render(bitmap, template, data, default, default)); Assert.Null(exception); @@ -83,7 +83,7 @@ public void WbReceipt_RendersWithBlackColumns() } [Fact] - public void WbReceipt_TextWithBackgroundButton_Renders() + public async Task WbReceipt_TextWithBackgroundButton_Renders() { const string yaml = """ canvas: @@ -103,7 +103,7 @@ public void WbReceipt_TextWithBackgroundButton_Renders() var data = new ObjectValue(); using var bitmap = new SKBitmap(200, 50); - var exception = Record.Exception(() => _renderer.Render(bitmap, template, data)); + var exception = await Record.ExceptionAsync(async () => await _renderer.Render(bitmap, template, data, default, default)); Assert.Null(exception); diff --git a/tests/FlexRender.Tests/Layout/FlexLayoutBugTests.cs b/tests/FlexRender.Tests/Layout/FlexLayoutBugTests.cs index 2bea316..32aa6fd 100644 --- a/tests/FlexRender.Tests/Layout/FlexLayoutBugTests.cs +++ b/tests/FlexRender.Tests/Layout/FlexLayoutBugTests.cs @@ -47,7 +47,7 @@ public void Dispose() /// See: docs/known-issues/layout-bugs.md /// [Fact] - public void AlignItemsEnd_AutoHeightRow_ChildrenAlignedToBottom() + public async Task AlignItemsEnd_AutoHeightRow_ChildrenAlignedToBottom() { // Arrange: Row container with align: end, no explicit height, 3 children of different heights. // Each child has a distinct background color so we can verify position via pixel checks. @@ -107,7 +107,7 @@ public void AlignItemsEnd_AutoHeightRow_ChildrenAlignedToBottom() // Act: Render to bitmap using var bitmap = new SKBitmap(180, 80); var data = new ObjectValue(); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Assert: Check pixel colors at strategic positions. @@ -224,7 +224,7 @@ public void AlignItemsEnd_AutoHeightRow_LayoutPositions() /// See: docs/known-issues/layout-bugs.md /// [Fact] - public void AlignItemsCenter_AutoHeightRow_ChildrenVerticallyCentered() + public async Task AlignItemsCenter_AutoHeightRow_ChildrenVerticallyCentered() { // Arrange: Row container with align: center, no explicit height, 3 children of different heights. // @@ -283,7 +283,7 @@ public void AlignItemsCenter_AutoHeightRow_ChildrenVerticallyCentered() // Act: Render to bitmap using var bitmap = new SKBitmap(180, 80); var data = new ObjectValue(); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Assert: Check pixel colors at strategic positions. @@ -408,7 +408,7 @@ public void AlignItemsCenter_AutoHeightRow_LayoutPositions() /// See: docs/known-issues/layout-bugs.md /// [Fact] - public void VerticalSeparator_InRowWithExplicitHeight_StretchesToContainerHeight() + public async Task VerticalSeparator_InRowWithExplicitHeight_StretchesToContainerHeight() { // Arrange: Row container (100px height) with two colored boxes and a vertical separator between them. // @@ -470,7 +470,7 @@ public void VerticalSeparator_InRowWithExplicitHeight_StretchesToContainerHeight // Act: Render to bitmap using var bitmap = new SKBitmap(200, 100); var data = new ObjectValue(); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Assert: The separator should be a visible black line spanning most of the height. // The separator is at approximately X=80..81 (2px wide). @@ -503,7 +503,7 @@ public void VerticalSeparator_InRowWithExplicitHeight_StretchesToContainerHeight /// See: docs/known-issues/layout-bugs.md /// [Fact] - public void VerticalSeparator_InAutoHeightRow_StretchesToContentHeight() + public async Task VerticalSeparator_InAutoHeightRow_StretchesToContentHeight() { // Arrange: Row container (auto height) with two colored boxes and a vertical separator. // @@ -560,7 +560,7 @@ public void VerticalSeparator_InAutoHeightRow_StretchesToContentHeight() // Act: Render to bitmap using var bitmap = new SKBitmap(200, 80); var data = new ObjectValue(); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Assert: The separator should span from Y=0 to Y=79 (80px total). // Check at multiple Y positions. diff --git a/tests/FlexRender.Tests/Rendering/RenderingIntegrationTests.cs b/tests/FlexRender.Tests/Rendering/RenderingIntegrationTests.cs index 7841f50..5c710b8 100644 --- a/tests/FlexRender.Tests/Rendering/RenderingIntegrationTests.cs +++ b/tests/FlexRender.Tests/Rendering/RenderingIntegrationTests.cs @@ -127,7 +127,7 @@ public async Task FullPipeline_MultipleFormats_AllWork() } [Fact] - public void FullPipeline_HeightFixed_CalculatesWidth() + public async Task FullPipeline_HeightFixed_CalculatesWidth() { const string yaml = """ canvas: @@ -142,7 +142,7 @@ public void FullPipeline_HeightFixed_CalculatesWidth() var template = _parser.Parse(yaml); var data = new ObjectValue(); - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); Assert.Equal(100f, size.Height); Assert.True(size.Width > 0); diff --git a/tests/FlexRender.Tests/Rendering/SkiaRendererRotationTests.cs b/tests/FlexRender.Tests/Rendering/SkiaRendererRotationTests.cs index ffc7f23..8639bb7 100644 --- a/tests/FlexRender.Tests/Rendering/SkiaRendererRotationTests.cs +++ b/tests/FlexRender.Tests/Rendering/SkiaRendererRotationTests.cs @@ -18,7 +18,7 @@ public void Dispose() } [Fact] - public void Render_WithRotateRight_SwapsDimensions() + public async Task Render_WithRotateRight_SwapsDimensions() { // Arrange: 300x100 canvas should become 100x300 after 90 degree rotation var template = new Template @@ -44,9 +44,9 @@ public void Render_WithRotateRight_SwapsDimensions() var data = new ObjectValue(); // Act - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Assert: dimensions should be swapped Assert.Equal(100, size.Width); @@ -56,7 +56,7 @@ public void Render_WithRotateRight_SwapsDimensions() } [Fact] - public void Render_WithRotateLeft_SwapsDimensions() + public async Task Render_WithRotateLeft_SwapsDimensions() { // Arrange: 300x100 canvas should become 100x300 after 270 degree rotation var template = new Template @@ -82,9 +82,9 @@ public void Render_WithRotateLeft_SwapsDimensions() var data = new ObjectValue(); // Act - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Assert: dimensions should be swapped Assert.Equal(100, size.Width); @@ -94,7 +94,7 @@ public void Render_WithRotateLeft_SwapsDimensions() } [Fact] - public void Render_WithRotateFlip_KeepsDimensions() + public async Task Render_WithRotateFlip_KeepsDimensions() { // Arrange: 300x100 canvas should remain 300x100 after 180 degree rotation var template = new Template @@ -120,9 +120,9 @@ public void Render_WithRotateFlip_KeepsDimensions() var data = new ObjectValue(); // Act - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Assert: dimensions should NOT be swapped Assert.Equal(300, size.Width); @@ -132,7 +132,7 @@ public void Render_WithRotateFlip_KeepsDimensions() } [Fact] - public void Render_WithRotateNone_DoesNothing() + public async Task Render_WithRotateNone_DoesNothing() { // Arrange: 300x100 canvas should remain 300x100 with no rotation var template = new Template @@ -158,9 +158,9 @@ public void Render_WithRotateNone_DoesNothing() var data = new ObjectValue(); // Act - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Assert: dimensions should NOT be swapped Assert.Equal(300, size.Width); @@ -170,7 +170,7 @@ public void Render_WithRotateNone_DoesNothing() } [Fact] - public void Measure_WithRotate90_ReturnsSwappedDimensions() + public async Task Measure_WithRotate90_ReturnsSwappedDimensions() { // Arrange: receipt template 630px wide, content height ~200px var template = new Template @@ -195,7 +195,7 @@ public void Measure_WithRotate90_ReturnsSwappedDimensions() var data = new ObjectValue(); // Act - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); // Assert: width and height should be swapped Assert.Equal(200, size.Width); @@ -203,7 +203,7 @@ public void Measure_WithRotate90_ReturnsSwappedDimensions() } [Fact] - public void Render_WithRotateRight_RotatesContentCorrectly() + public async Task Render_WithRotateRight_RotatesContentCorrectly() { // Arrange: Place a red element at top-left, after 90 CW rotation it should be at top-right var template = new Template @@ -238,9 +238,9 @@ public void Render_WithRotateRight_RotatesContentCorrectly() var data = new ObjectValue(); // Act - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Assert: After 90 CW rotation, original top-left becomes top-right // The red element was at (0-20, 0-20) in original 100x40 @@ -252,7 +252,7 @@ public void Render_WithRotateRight_RotatesContentCorrectly() } [Fact] - public void Render_WithRotateLeft_RotatesContentCorrectly() + public async Task Render_WithRotateLeft_RotatesContentCorrectly() { // Arrange: Similar setup for 270 degree (left) rotation var template = new Template @@ -285,9 +285,9 @@ public void Render_WithRotateLeft_RotatesContentCorrectly() var data = new ObjectValue(); // Act - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Assert: After 270 CCW rotation Assert.Equal(40, size.Width); @@ -295,7 +295,7 @@ public void Render_WithRotateLeft_RotatesContentCorrectly() } [Fact] - public void Render_ThermalPrinterScenario_RotatesReceiptCorrectly() + public async Task Render_ThermalPrinterScenario_RotatesReceiptCorrectly() { // Arrange: Thermal printer receipt scenario // Original: 630px wide, ~300px tall receipt @@ -341,9 +341,9 @@ public void Render_ThermalPrinterScenario_RotatesReceiptCorrectly() var data = new ObjectValue(); // Act - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); using var bitmap = new SKBitmap((int)size.Width, (int)size.Height); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Assert: Width and height should be swapped for thermal printer Assert.Equal(300, size.Width); @@ -353,7 +353,7 @@ public void Render_ThermalPrinterScenario_RotatesReceiptCorrectly() } [Fact] - public void Render_WithDefaultRotation_DoesNotRotate() + public async Task Render_WithDefaultRotation_DoesNotRotate() { // Arrange: Default CanvasSettings has Rotate = "none" var template = new Template @@ -377,7 +377,7 @@ public void Render_WithDefaultRotation_DoesNotRotate() var data = new ObjectValue(); // Act - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); // Assert: No dimension swap Assert.Equal(200, size.Width); @@ -392,7 +392,7 @@ public void Render_WithDefaultRotation_DoesNotRotate() [InlineData("90")] [InlineData("180")] [InlineData("270")] - public void Render_VariousRotationValues_DoesNotThrow(string rotateValue) + public async Task Render_VariousRotationValues_DoesNotThrow(string rotateValue) { // Arrange var template = new Template @@ -412,11 +412,11 @@ public void Render_VariousRotationValues_DoesNotThrow(string rotateValue) var data = new ObjectValue(); // Act & Assert: Should not throw - var exception = Record.Exception(() => + var exception = await Record.ExceptionAsync(async () => { - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); using var bitmap = new SKBitmap((int)Math.Max(size.Width, 1), (int)Math.Max(size.Height, 1)); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); }); Assert.Null(exception); diff --git a/tests/FlexRender.Tests/Rendering/SkiaRendererTests.cs b/tests/FlexRender.Tests/Rendering/SkiaRendererTests.cs index dfb3eaf..bedec6e 100644 --- a/tests/FlexRender.Tests/Rendering/SkiaRendererTests.cs +++ b/tests/FlexRender.Tests/Rendering/SkiaRendererTests.cs @@ -1,7 +1,9 @@ +using FlexRender.Abstractions; using FlexRender.Configuration; using FlexRender.Parsing; using FlexRender.Parsing.Ast; using FlexRender.Rendering; +using FlexRender.TemplateEngine; using SkiaSharp; using Xunit; @@ -27,7 +29,7 @@ public void Constructor_CreatesInstance() } [Fact] - public void Measure_SimpleTemplate_ReturnsDimensions() + public async Task Measure_SimpleTemplate_ReturnsDimensions() { var template = new Template { @@ -39,14 +41,14 @@ public void Measure_SimpleTemplate_ReturnsDimensions() }; var data = new ObjectValue(); - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); Assert.Equal(300f, size.Width); Assert.True(size.Height > 0); } [Fact] - public void Measure_EmptyTemplate_ReturnsMinimalSize() + public async Task Measure_EmptyTemplate_ReturnsMinimalSize() { var template = new Template { @@ -55,14 +57,14 @@ public void Measure_EmptyTemplate_ReturnsMinimalSize() }; var data = new ObjectValue(); - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); Assert.Equal(300f, size.Width); Assert.True(size.Height >= 0); } [Fact] - public void Measure_WithHeightFixed_ReturnsCorrectDimensions() + public async Task Measure_WithHeightFixed_ReturnsCorrectDimensions() { var template = new Template { @@ -74,19 +76,15 @@ public void Measure_WithHeightFixed_ReturnsCorrectDimensions() }; var data = new ObjectValue(); - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); Assert.Equal(500f, size.Height); Assert.True(size.Width > 0); } [Fact] - public void Render_ToCanvas_DrawsWithoutError() + public async Task Render_ToCanvas_DrawsWithoutError() { - using var bitmap = new SKBitmap(300, 100); - using var canvas = new SKCanvas(bitmap); - canvas.Clear(SKColors.White); - var template = new Template { Canvas = new CanvasSettings { Width = 300 }, @@ -97,18 +95,18 @@ public void Render_ToCanvas_DrawsWithoutError() }; var data = new ObjectValue(); - var exception = Record.Exception(() => - _renderer.Render(canvas, template, data)); + var exception = await Record.ExceptionAsync(async () => + { + using var bitmap = await _renderer.Render(template, data); + }); Assert.Null(exception); } [Fact] - public void Render_ToCanvas_WithOffset_AppliesOffset() + public async Task Render_ToCanvas_WithOffset_AppliesOffset() { using var bitmap = new SKBitmap(300, 100); - using var canvas = new SKCanvas(bitmap); - canvas.Clear(SKColors.White); var template = new Template { @@ -121,14 +119,14 @@ public void Render_ToCanvas_WithOffset_AppliesOffset() var data = new ObjectValue(); var offset = new SKPoint(50, 25); - var exception = Record.Exception(() => - _renderer.Render(canvas, template, data, offset)); + var exception = await Record.ExceptionAsync(async () => + await _renderer.Render(bitmap, template, data, offset, default)); Assert.Null(exception); } [Fact] - public void Render_ToBitmap_DrawsWithoutError() + public async Task Render_ToBitmap_DrawsWithoutError() { using var bitmap = new SKBitmap(300, 100); @@ -142,14 +140,14 @@ public void Render_ToBitmap_DrawsWithoutError() }; var data = new ObjectValue(); - var exception = Record.Exception(() => - _renderer.Render(bitmap, template, data)); + var exception = await Record.ExceptionAsync(async () => + await _renderer.Render(bitmap, template, data, default, default)); Assert.Null(exception); } [Fact] - public void Render_WithBackground_FillsBackground() + public async Task Render_WithBackground_FillsBackground() { using var bitmap = new SKBitmap(100, 100); @@ -164,7 +162,7 @@ public void Render_WithBackground_FillsBackground() }; var data = new ObjectValue(); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Check a pixel in the upper area where background should be rendered var topPixel = bitmap.GetPixel(50, 10); @@ -174,7 +172,7 @@ public void Render_WithBackground_FillsBackground() } [Fact] - public void Render_WithDataSubstitution_SubstitutesValues() + public async Task Render_WithDataSubstitution_SubstitutesValues() { using var bitmap = new SKBitmap(300, 50); @@ -189,14 +187,14 @@ public void Render_WithDataSubstitution_SubstitutesValues() var data = new ObjectValue { ["name"] = "World" }; // This should not throw - full text content verification would be in snapshot tests - var exception = Record.Exception(() => - _renderer.Render(bitmap, template, data)); + var exception = await Record.ExceptionAsync(async () => + await _renderer.Render(bitmap, template, data, default, default)); Assert.Null(exception); } [Fact] - public void Render_ParsedYaml_RendersCorrectly() + public async Task Render_ParsedYaml_RendersCorrectly() { const string yaml = """ canvas: @@ -212,14 +210,14 @@ public void Render_ParsedYaml_RendersCorrectly() var data = new ObjectValue(); using var bitmap = new SKBitmap(200, 50); - var exception = Record.Exception(() => - _renderer.Render(bitmap, template, data)); + var exception = await Record.ExceptionAsync(async () => + await _renderer.Render(bitmap, template, data, default, default)); Assert.Null(exception); } [Fact] - public void Render_MultipleTextElements_DrawsAll() + public async Task Render_MultipleTextElements_DrawsAll() { using var bitmap = new SKBitmap(300, 150); @@ -235,14 +233,14 @@ public void Render_MultipleTextElements_DrawsAll() }; var data = new ObjectValue(); - var exception = Record.Exception(() => - _renderer.Render(bitmap, template, data)); + var exception = await Record.ExceptionAsync(async () => + await _renderer.Render(bitmap, template, data, default, default)); Assert.Null(exception); } [Fact] - public void Render_WithITemplateData_WorksCorrectly() + public async Task Render_WithITemplateData_WorksCorrectly() { using var bitmap = new SKBitmap(300, 50); @@ -256,8 +254,8 @@ public void Render_WithITemplateData_WorksCorrectly() }; var data = new TestTemplateData { Price = 99.99m }; - var exception = Record.Exception(() => - _renderer.Render(bitmap, template, data)); + var exception = await Record.ExceptionAsync(async () => + await _renderer.Render(bitmap, template, data.ToTemplateValue(), default, default)); Assert.Null(exception); } @@ -273,7 +271,7 @@ public ObjectValue ToTemplateValue() } [Fact] - public void Render_FlexWithBackground_DrawsBackground() + public async Task Render_FlexWithBackground_DrawsBackground() { using var bitmap = new SKBitmap(100, 100); @@ -293,7 +291,7 @@ public void Render_FlexWithBackground_DrawsBackground() }; var data = new ObjectValue(); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Check pixel in the flex area (should be red) var pixel = bitmap.GetPixel(50, 25); @@ -303,7 +301,7 @@ public void Render_FlexWithBackground_DrawsBackground() } [Fact] - public void Render_TextWithBackground_DrawsBackground() + public async Task Render_TextWithBackground_DrawsBackground() { using var bitmap = new SKBitmap(200, 100); @@ -324,7 +322,7 @@ public void Render_TextWithBackground_DrawsBackground() }; var data = new ObjectValue(); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Check that green background was drawn in text area (bottom-left region below text line) var pixel = bitmap.GetPixel(10, 40); @@ -332,7 +330,7 @@ public void Render_TextWithBackground_DrawsBackground() } [Fact] - public void Render_ElementWithoutBackground_NoBackgroundDrawn() + public async Task Render_ElementWithoutBackground_NoBackgroundDrawn() { using var bitmap = new SKBitmap(100, 100); @@ -352,7 +350,7 @@ public void Render_ElementWithoutBackground_NoBackgroundDrawn() }; var data = new ObjectValue(); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // Pixel should be white (canvas background), not anything else var pixel = bitmap.GetPixel(25, 25); @@ -365,7 +363,7 @@ public void Render_ElementWithoutBackground_NoBackgroundDrawn() /// Verifies that a font named "default" is also registered as "main". /// [Fact] - public void Render_DefaultFont_RegisteredAsMain() + public async Task Render_DefaultFont_RegisteredAsMain() { const string yaml = """ fonts: @@ -384,8 +382,8 @@ public void Render_DefaultFont_RegisteredAsMain() // Should not throw - the "default" font is registered as "main" // which is the default font reference for TextElement - var exception = Record.Exception(() => - _renderer.Render(bitmap, template, data)); + var exception = await Record.ExceptionAsync(async () => + await _renderer.Render(bitmap, template, data, default, default)); Assert.Null(exception); } @@ -394,7 +392,7 @@ public void Render_DefaultFont_RegisteredAsMain() /// Verifies that text elements without explicit font use the default font. /// [Fact] - public void Render_TextWithoutExplicitFont_UsesDefaultFont() + public async Task Render_TextWithoutExplicitFont_UsesDefaultFont() { const string yaml = """ fonts: @@ -416,8 +414,8 @@ public void Render_TextWithoutExplicitFont_UsesDefaultFont() using var bitmap = new SKBitmap(300, 100); // Both text elements should render without error - var exception = Record.Exception(() => - _renderer.Render(bitmap, template, data)); + var exception = await Record.ExceptionAsync(async () => + await _renderer.Render(bitmap, template, data, default, default)); Assert.Null(exception); @@ -440,7 +438,7 @@ public void Constructor_WithResourceLimits_SetsMaxRenderDepth() } [Fact] - public void Constructor_DefaultLimits_Uses100MaxRenderDepth() + public async Task Constructor_DefaultLimits_Uses100MaxRenderDepth() { // The parameterless constructor should use 100 as default using var renderer = new SkiaRenderer(); @@ -456,12 +454,12 @@ public void Constructor_DefaultLimits_Uses100MaxRenderDepth() }; var data = new ObjectValue(); - var size = renderer.Measure(template, data); + var size = await renderer.MeasureAsync(template, data); Assert.True(size.Width > 0); } [Fact] - public void Render_SeparatorWithTemplateColorExpression_ResolvesColor() + public async Task Render_SeparatorWithTemplateColorExpression_ResolvesColor() { var template = new Template { @@ -482,9 +480,9 @@ public void Render_SeparatorWithTemplateColorExpression_ResolvesColor() }; // Measure first to get correct bitmap size - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); using var bitmap = new SKBitmap((int)size.Width, Math.Max((int)size.Height, 2)); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); // The separator line is drawn at y + height/2; with thickness=2, height=2, so lineY=1 var pixel = bitmap.GetPixel(100, 1); @@ -498,7 +496,7 @@ public void Render_SeparatorWithTemplateColorExpression_ResolvesColor() /// to a system default font. /// [Fact] - public void Render_TextWithNoFontsSection_UsesDefaultFallback() + public async Task Render_TextWithNoFontsSection_UsesDefaultFallback() { var template = new Template { @@ -518,8 +516,8 @@ public void Render_TextWithNoFontsSection_UsesDefaultFallback() using var bitmap = new SKBitmap(200, 100); // Should not throw NullReferenceException even though no fonts are configured - var exception = Record.Exception(() => - _renderer.Render(bitmap, template, data)); + var exception = await Record.ExceptionAsync(async () => + await _renderer.Render(bitmap, template, data, default, default)); Assert.Null(exception); } @@ -558,7 +556,7 @@ public async Task RenderToPng_NoFontsSection_DoesNotThrow() /// Verifies that parsed YAML with text but no fonts section renders correctly. /// [Fact] - public void Render_ParsedYamlNoFonts_RendersCorrectly() + public async Task Render_ParsedYamlNoFonts_RendersCorrectly() { const string yaml = """ canvas: @@ -577,8 +575,8 @@ public void Render_ParsedYamlNoFonts_RendersCorrectly() using var bitmap = new SKBitmap(200, 100); - var exception = Record.Exception(() => - _renderer.Render(bitmap, template, data)); + var exception = await Record.ExceptionAsync(async () => + await _renderer.Render(bitmap, template, data, default, default)); Assert.Null(exception); } @@ -588,7 +586,7 @@ public void Render_ParsedYamlNoFonts_RendersCorrectly() /// NRE when no fonts section is present. /// [Fact] - public void Render_MultipleTextNoFonts_DoesNotThrow() + public async Task Render_MultipleTextNoFonts_DoesNotThrow() { var template = new Template { @@ -604,9 +602,82 @@ public void Render_MultipleTextNoFonts_DoesNotThrow() using var bitmap = new SKBitmap(300, 200); - var exception = Record.Exception(() => - _renderer.Render(bitmap, template, data)); + var exception = await Record.ExceptionAsync(async () => + await _renderer.Render(bitmap, template, data, default, default)); + + Assert.Null(exception); + } + + /// + /// Regression test: ContentElement previously threw TemplateEngineException + /// "Content element expansion requires async processing" when render pipeline + /// used sync Expand() instead of ExpandAsync(). + /// + [Fact] + public async Task ContentElement_RendersSuccessfully_ThroughAsyncPipeline() + { + // Arrange: register a simple content parser that expands to text elements + var registry = new ContentParserRegistry(); + registry.Register(new InlineTestContentParser()); + + using var renderer = new SkiaRenderer( + new ResourceLimits(), + qrProvider: null, + barcodeProvider: null, + imageLoader: null, + contentParserRegistry: registry); + + var template = new Template + { + Canvas = new CanvasSettings { Width = 300, Background = "#ffffff" }, + Elements = new List + { + new FlexElement + { + Children = new List + { + new TextElement { Content = "Before content" }, + new ContentElement { Source = "Hello|World", Format = "pipe-split" }, + new TextElement { Content = "After content" } + } + } + } + }; + var data = new ObjectValue(); + + // Act & Assert: should not throw TemplateEngineException about sync Expand() + var exception = await Record.ExceptionAsync(async () => + { + using var bitmap = await renderer.Render(template, data); + Assert.NotNull(bitmap); + Assert.True(bitmap.Width > 0); + Assert.True(bitmap.Height > 0); + }); Assert.Null(exception); } + + /// + /// Simple content parser that splits text on pipe characters and creates a + /// for each segment. Used by regression tests. + /// + private sealed class InlineTestContentParser : IContentParser + { + /// + public string FormatName => "pipe-split"; + + /// + public IReadOnlyList Parse( + string text, + ContentParserContext context, + IReadOnlyDictionary? options = null) + { + ArgumentNullException.ThrowIfNull(text); + if (string.IsNullOrWhiteSpace(text)) return []; + + return text.Split('|', StringSplitOptions.RemoveEmptyEntries) + .Select(segment => (TemplateElement)new TextElement { Content = segment.Trim() }) + .ToList(); + } + } } diff --git a/tests/FlexRender.Tests/Rendering/SkiaTextShaperIntegrationTests.cs b/tests/FlexRender.Tests/Rendering/SkiaTextShaperIntegrationTests.cs index b2c3887..ddb295f 100644 --- a/tests/FlexRender.Tests/Rendering/SkiaTextShaperIntegrationTests.cs +++ b/tests/FlexRender.Tests/Rendering/SkiaTextShaperIntegrationTests.cs @@ -18,7 +18,7 @@ public SkiaTextShaperIntegrationTests() } [Fact] - public void ComputeLayout_PopulatesTextLinesFromSkiaTextShaper() + public async Task ComputeLayout_PopulatesTextLinesFromSkiaTextShaper() { var template = new Template { @@ -29,7 +29,7 @@ public void ComputeLayout_PopulatesTextLinesFromSkiaTextShaper() } }; - var root = _renderer.ComputeLayout(template, new ObjectValue()); + var root = await _renderer.ComputeLayoutAsync(template, new ObjectValue()); var textNode = root.Children[0]; Assert.NotNull(textNode.TextLines); @@ -38,7 +38,7 @@ public void ComputeLayout_PopulatesTextLinesFromSkiaTextShaper() } [Fact] - public void ComputeLayout_WrappedText_ProducesMultipleLines() + public async Task ComputeLayout_WrappedText_ProducesMultipleLines() { var template = new Template { @@ -54,7 +54,7 @@ public void ComputeLayout_WrappedText_ProducesMultipleLines() } }; - var root = _renderer.ComputeLayout(template, new ObjectValue()); + var root = await _renderer.ComputeLayoutAsync(template, new ObjectValue()); var textNode = root.Children[0]; Assert.NotNull(textNode.TextLines); diff --git a/tests/FlexRender.Tests/Rendering/SvgQrRenderingTests.cs b/tests/FlexRender.Tests/Rendering/SvgQrRenderingTests.cs index 1a268d7..fbe8970 100644 --- a/tests/FlexRender.Tests/Rendering/SvgQrRenderingTests.cs +++ b/tests/FlexRender.Tests/Rendering/SvgQrRenderingTests.cs @@ -21,13 +21,13 @@ public sealed class SvgQrRenderingTests /// using the dedicated QrSvgProvider. /// [Fact] - public void RenderToSvg_QrElement_ProducesSvgPathNotBase64() + public async Task RenderToSvg_QrElement_ProducesSvgPathNotBase64() { var svgProvider = new QrSvgProvider(); var engine = CreateEngineWithSvgProvider(svgProvider); var template = CreateTemplateWithQr("https://example.com"); - var svg = engine.RenderToSvg(template, new ObjectValue()); + var svg = engine.Engine.RenderToSvgFromProcessed(await engine.Pipeline.ProcessAsync(template, new ObjectValue())); // Should contain native SVG path, not base64 image Assert.Contains(" [Fact] - public void RenderToSvg_QrElementWithBitmapOnlyProvider_ProducesBase64Image() + public async Task RenderToSvg_QrElementWithBitmapOnlyProvider_ProducesBase64Image() { var bitmapOnlyProvider = new BitmapOnlyQrProvider(); var engine = CreateEngineWithRasterProvider(bitmapOnlyProvider); var template = CreateTemplateWithQr("https://example.com"); - var svg = engine.RenderToSvg(template, new ObjectValue()); + var svg = engine.Engine.RenderToSvgFromProcessed(await engine.Pipeline.ProcessAsync(template, new ObjectValue())); // Should fall back to base64 image Assert.Contains("data:image/png;base64", svg); @@ -57,13 +57,13 @@ public void RenderToSvg_QrElementWithBitmapOnlyProvider_ProducesBase64Image() /// Verifies the SVG output contains the correct foreground color. /// [Fact] - public void RenderToSvg_QrElementWithCustomColor_UsesForegroundColor() + public async Task RenderToSvg_QrElementWithCustomColor_UsesForegroundColor() { var svgProvider = new QrSvgProvider(); var engine = CreateEngineWithSvgProvider(svgProvider); var template = CreateTemplateWithQr("Hello", foreground: "#ff0000"); - var svg = engine.RenderToSvg(template, new ObjectValue()); + var svg = engine.Engine.RenderToSvgFromProcessed(await engine.Pipeline.ProcessAsync(template, new ObjectValue())); Assert.Contains("fill=\"#ff0000\"", svg); } @@ -72,45 +72,49 @@ public void RenderToSvg_QrElementWithCustomColor_UsesForegroundColor() /// Verifies the SVG output contains background rect when background is set. /// [Fact] - public void RenderToSvg_QrElementWithBackground_ContainsBackgroundRect() + public async Task RenderToSvg_QrElementWithBackground_ContainsBackgroundRect() { var svgProvider = new QrSvgProvider(); var engine = CreateEngineWithSvgProvider(svgProvider); var template = CreateTemplateWithQr("Hello", background: "#ffffff"); - var svg = engine.RenderToSvg(template, new ObjectValue()); + var svg = engine.Engine.RenderToSvgFromProcessed(await engine.Pipeline.ProcessAsync(template, new ObjectValue())); Assert.Contains("fill=\"#ffffff\"", svg); } - private static SvgRenderingEngine CreateEngineWithSvgProvider(ISvgContentProvider svgProvider) + private static (SvgRenderingEngine Engine, TemplatePipeline Pipeline) CreateEngineWithSvgProvider(ISvgContentProvider svgProvider) { var limits = new ResourceLimits(); var expander = new TemplateExpander(limits); var pipeline = new TemplatePipeline(expander, new TemplateProcessor(limits)); var layoutEngine = new LayoutEngine(limits); - return new SvgRenderingEngine( + var engine = new SvgRenderingEngine( limits, pipeline, layoutEngine, baseFontSize: 16f, qrSvgProvider: svgProvider); + + return (engine, pipeline); } - private static SvgRenderingEngine CreateEngineWithRasterProvider(IContentProvider rasterProvider) + private static (SvgRenderingEngine Engine, TemplatePipeline Pipeline) CreateEngineWithRasterProvider(IContentProvider rasterProvider) { var limits = new ResourceLimits(); var expander = new TemplateExpander(limits); var pipeline = new TemplatePipeline(expander, new TemplateProcessor(limits)); var layoutEngine = new LayoutEngine(limits); - return new SvgRenderingEngine( + var engine = new SvgRenderingEngine( limits, pipeline, layoutEngine, baseFontSize: 16f, qrProvider: rasterProvider); + + return (engine, pipeline); } private static Template CreateTemplateWithQr( diff --git a/tests/FlexRender.Tests/Snapshots/NdcSnapshotTests.cs b/tests/FlexRender.Tests/Snapshots/NdcSnapshotTests.cs index 474cdfe..fcf2409 100644 --- a/tests/FlexRender.Tests/Snapshots/NdcSnapshotTests.cs +++ b/tests/FlexRender.Tests/Snapshots/NdcSnapshotTests.cs @@ -75,7 +75,7 @@ private static string GetFontsBasePath() /// Uses auto font size (24pt from 576 / (40 * 0.6)). /// [Fact] - public void NdcReceipt_SimpleRussianAscii() + public async Task NdcReceipt_SimpleRussianAscii() { var ndcData = ":02\x1b(1 \x1b(Intcnjdsq ~fyr f\x1b(1\r\n" + " \x1b(Intk\x1b(1. 8 (800) 000-00-00\r\n" + @@ -91,7 +91,7 @@ public void NdcReceipt_SimpleRussianAscii() foreach (var el in elements) template.AddElement(el); - AssertSnapshot("ndc_receipt_simple", template, new ObjectValue()); + await AssertSnapshot("ndc_receipt_simple", template, new ObjectValue()); } /// @@ -100,7 +100,7 @@ public void NdcReceipt_SimpleRussianAscii() /// Uses auto font size (24pt from 576 / (40 * 0.6)). /// [Fact] - public void NdcReceipt_WithFormFeed() + public async Task NdcReceipt_WithFormFeed() { var ndcData = "\x1b(1PAGE 1 CONTENT\r\n" + "Line 2\x0c" + @@ -120,7 +120,7 @@ public void NdcReceipt_WithFormFeed() foreach (var el in elements) template.AddElement(el); - AssertSnapshot("ndc_receipt_formfeed", template, new ObjectValue()); + await AssertSnapshot("ndc_receipt_formfeed", template, new ObjectValue()); } /// @@ -129,7 +129,7 @@ public void NdcReceipt_WithFormFeed() /// Uses auto font size (24pt from 576 / (40 * 0.6)). /// [Fact] - public void NdcReceipt_WithSpacing() + public async Task NdcReceipt_WithSpacing() { var ndcData = "\x1b(1Label\x0e5Value\r\n" + "Left\x0e9Right"; @@ -147,7 +147,7 @@ public void NdcReceipt_WithSpacing() foreach (var el in elements) template.AddElement(el); - AssertSnapshot("ndc_receipt_spacing", template, new ObjectValue()); + await AssertSnapshot("ndc_receipt_spacing", template, new ObjectValue()); } /// @@ -155,7 +155,7 @@ public void NdcReceipt_WithSpacing() /// Charset > renders bold text at 48pt (double of auto 24); charset 1 uses auto 24pt. /// [Fact] - public void NdcReceipt_DoubleSizeCharset() + public async Task NdcReceipt_DoubleSizeCharset() { var ndcData = "\x1b(>HEADER\r\n" + "\x1b(1Normal text\r\n" + @@ -188,7 +188,7 @@ public void NdcReceipt_DoubleSizeCharset() foreach (var el in elements) template.AddElement(el); - AssertSnapshot("ndc_receipt_doublesize", template, new ObjectValue()); + await AssertSnapshot("ndc_receipt_doublesize", template, new ObjectValue()); } /// @@ -196,7 +196,7 @@ public void NdcReceipt_DoubleSizeCharset() /// Uses auto font size (24pt from 576 / (40 * 0.6)). /// [Fact] - public void NdcReceipt_BankAMiniStatement() + public async Task NdcReceipt_BankAMiniStatement() { var text = LoadTestData("bank-a-mini-statement.bin"); @@ -208,7 +208,7 @@ public void NdcReceipt_BankAMiniStatement() foreach (var el in elements) template.AddElement(el); - AssertSnapshot("ndc_receipt_bank_a", template, new ObjectValue(), maxDifferencePercent: 7.0); + await AssertSnapshot("ndc_receipt_bank_a", template, new ObjectValue(), maxDifferencePercent: 7.0); } /// @@ -217,7 +217,7 @@ public void NdcReceipt_BankAMiniStatement() /// Uses 44 columns with auto font size (~21pt from 576 / (44 * 0.6)). /// [Fact] - public void NdcReceipt_BankEBalance() + public async Task NdcReceipt_BankEBalance() { var text = LoadTestData("bank-e-balance.bin"); @@ -229,7 +229,7 @@ public void NdcReceipt_BankEBalance() foreach (var el in elements) template.AddElement(el); - AssertSnapshot("ndc_receipt_bank_e", template, new ObjectValue()); + await AssertSnapshot("ndc_receipt_bank_e", template, new ObjectValue()); } /// @@ -237,7 +237,7 @@ public void NdcReceipt_BankEBalance() /// Uses auto font size (24pt from 576 / (40 * 0.6)). /// [Fact] - public void NdcReceipt_BankCBalance() + public async Task NdcReceipt_BankCBalance() { var text = LoadTestData("bank-c-balance-receipt.bin"); @@ -249,7 +249,7 @@ public void NdcReceipt_BankCBalance() foreach (var el in elements) template.AddElement(el); - AssertSnapshot("ndc_receipt_bank_c_balance", template, new ObjectValue(), maxDifferencePercent: 7.0); + await AssertSnapshot("ndc_receipt_bank_c_balance", template, new ObjectValue(), maxDifferencePercent: 7.0); } /// @@ -257,7 +257,7 @@ public void NdcReceipt_BankCBalance() /// Uses auto font size (24pt from 576 / (40 * 0.6)). /// [Fact] - public void NdcReceipt_BankCStatement() + public async Task NdcReceipt_BankCStatement() { var text = LoadTestData("bank-c-statement-receipt.bin"); @@ -269,7 +269,7 @@ public void NdcReceipt_BankCStatement() foreach (var el in elements) template.AddElement(el); - AssertSnapshot("ndc_receipt_bank_c_statement", template, new ObjectValue()); + await AssertSnapshot("ndc_receipt_bank_c_statement", template, new ObjectValue()); } /// @@ -277,7 +277,7 @@ public void NdcReceipt_BankCStatement() /// Uses auto font size (24pt from 576 / (40 * 0.6)). /// [Fact] - public void NdcReceipt_BankDBalance() + public async Task NdcReceipt_BankDBalance() { var text = LoadTestData("bank-d-balance-receipt.bin"); @@ -312,7 +312,7 @@ public void NdcReceipt_BankDBalance() foreach (var el in elements) template.AddElement(el); - AssertSnapshot("ndc_receipt_bank_d_balance", template, new ObjectValue()); + await AssertSnapshot("ndc_receipt_bank_d_balance", template, new ObjectValue()); } /// @@ -320,7 +320,7 @@ public void NdcReceipt_BankDBalance() /// Uses auto font size (24pt from 576 / (40 * 0.6)). /// [Fact] - public void NdcReceipt_BankACashout() + public async Task NdcReceipt_BankACashout() { var text = LoadTestData("bank-a-cashout-receipt.bin"); @@ -332,7 +332,7 @@ public void NdcReceipt_BankACashout() foreach (var el in elements) template.AddElement(el); - AssertSnapshot("ndc_receipt_bank_a_cashout", template, new ObjectValue(), maxDifferencePercent: 7.0); + await AssertSnapshot("ndc_receipt_bank_a_cashout", template, new ObjectValue(), maxDifferencePercent: 7.0); } /// @@ -341,7 +341,7 @@ public void NdcReceipt_BankACashout() /// Uses auto font size (24pt from 576 / (40 * 0.6)). /// [Fact] - public void NdcReceipt_BankBBalance() + public async Task NdcReceipt_BankBBalance() { var text = LoadTestData("bank-b-balance-receipt.bin", System.Text.Encoding.UTF8); @@ -358,7 +358,7 @@ public void NdcReceipt_BankBBalance() foreach (var el in elements) template.AddElement(el); - AssertSnapshot("ndc_receipt_bank_b_balance", template, new ObjectValue()); + await AssertSnapshot("ndc_receipt_bank_b_balance", template, new ObjectValue()); } /// @@ -366,7 +366,7 @@ public void NdcReceipt_BankBBalance() /// No explicit font_size on charsets -- auto = 576 / (40 * 0.6) = 24. /// [Fact] - public void NdcReceipt_AutoFontSize() + public async Task NdcReceipt_AutoFontSize() { var ndcData = ":02\x1b(1 \x1b(Intcnjdsq ~fyr f\x1b(1\r\n" + " \x1b(Intk\x1b(1. 8 (800) 000-00-00\r\n" + @@ -402,7 +402,7 @@ public void NdcReceipt_AutoFontSize() foreach (var el in elements) template.AddElement(el); - AssertSnapshot("ndc_receipt_autofont", template, new ObjectValue()); + await AssertSnapshot("ndc_receipt_autofont", template, new ObjectValue()); } /// @@ -412,7 +412,7 @@ public void NdcReceipt_AutoFontSize() /// is embedded within a larger composed template with non-NDC elements and decorations. /// [Fact] - public void NdcReceipt_CompositeWithHeaderFooter() + public async Task NdcReceipt_CompositeWithHeaderFooter() { var text = LoadTestData("bank-a-cashout-receipt.bin"); @@ -484,7 +484,7 @@ public void NdcReceipt_CompositeWithHeaderFooter() template.AddElement(card); - AssertSnapshot("ndc_receipt_composite", template, new ObjectValue()); + await AssertSnapshot("ndc_receipt_composite", template, new ObjectValue()); } /// diff --git a/tests/FlexRender.Tests/Snapshots/SeparatorSnapshotTests.cs b/tests/FlexRender.Tests/Snapshots/SeparatorSnapshotTests.cs index d544e91..9fd04d4 100644 --- a/tests/FlexRender.Tests/Snapshots/SeparatorSnapshotTests.cs +++ b/tests/FlexRender.Tests/Snapshots/SeparatorSnapshotTests.cs @@ -17,7 +17,7 @@ public sealed class SeparatorSnapshotTests : SnapshotTestBase /// Tests horizontal separators with all three styles: dotted, dashed, and solid. /// [Fact] - public void SeparatorHorizontal_AllStyles() + public async Task SeparatorHorizontal_AllStyles() { var template = CreateTemplate(300); @@ -57,14 +57,14 @@ public void SeparatorHorizontal_AllStyles() template.AddElement(flex); - AssertSnapshot("separator_horizontal_all_styles", template, new ObjectValue()); + await AssertSnapshot("separator_horizontal_all_styles", template, new ObjectValue()); } /// /// Tests vertical separators with all three styles: dotted, dashed, and solid. /// [Fact] - public void SeparatorVertical_AllStyles() + public async Task SeparatorVertical_AllStyles() { var template = CreateTemplate(300); @@ -108,7 +108,7 @@ public void SeparatorVertical_AllStyles() template.AddElement(flex); - AssertSnapshot("separator_vertical_all_styles", template, new ObjectValue()); + await AssertSnapshot("separator_vertical_all_styles", template, new ObjectValue()); } /// diff --git a/tests/FlexRender.Tests/Snapshots/SnapshotTestBase.cs b/tests/FlexRender.Tests/Snapshots/SnapshotTestBase.cs index 93e54a6..494d364 100644 --- a/tests/FlexRender.Tests/Snapshots/SnapshotTestBase.cs +++ b/tests/FlexRender.Tests/Snapshots/SnapshotTestBase.cs @@ -139,7 +139,7 @@ protected SnapshotTestBase() /// /// Thrown when the rendered image does not match the golden image and update mode is disabled. /// - protected void AssertSnapshot( + protected async Task AssertSnapshot( string testName, Template template, ObjectValue data, @@ -153,7 +153,7 @@ protected void AssertSnapshot( ObjectDisposedException.ThrowIf(_disposed, this); // Render the template to a bitmap - using var actualBitmap = RenderTemplate(template, data); + using var actualBitmap = await RenderTemplate(template, data); if (_updateSnapshots) { @@ -170,9 +170,9 @@ protected void AssertSnapshot( /// The template to render. /// The data for variable substitution. /// A bitmap containing the rendered template. - private SKBitmap RenderTemplate(Template template, ObjectValue data) + private async Task RenderTemplate(Template template, ObjectValue data) { - var size = _renderer.Measure(template, data); + var size = await _renderer.MeasureAsync(template, data); var width = (int)Math.Ceiling(size.Width); var height = (int)Math.Ceiling(size.Height); @@ -181,7 +181,7 @@ private SKBitmap RenderTemplate(Template template, ObjectValue data) height = Math.Max(height, 1); var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); - _renderer.Render(bitmap, template, data); + await _renderer.Render(bitmap, template, data, default, default); return bitmap; } diff --git a/tests/FlexRender.Tests/Snapshots/SvgSnapshotTestBase.cs b/tests/FlexRender.Tests/Snapshots/SvgSnapshotTestBase.cs index 9792219..7dad5c5 100644 --- a/tests/FlexRender.Tests/Snapshots/SvgSnapshotTestBase.cs +++ b/tests/FlexRender.Tests/Snapshots/SvgSnapshotTestBase.cs @@ -67,6 +67,7 @@ public abstract class SvgSnapshotTestBase private const string OutputDirectoryName = "output"; private readonly SvgRenderingEngine _engine; + private readonly TemplatePipeline _pipeline; private readonly string _snapshotsBasePath; private readonly bool _updateSnapshots; @@ -81,12 +82,12 @@ protected SvgSnapshotTestBase() var limits = new ResourceLimits(); var expander = new TemplateExpander(limits); - var pipeline = new TemplatePipeline(expander, new TemplateProcessor(limits)); + _pipeline = new TemplatePipeline(expander, new TemplateProcessor(limits)); var layoutEngine = new LayoutEngine(limits); _engine = new SvgRenderingEngine( limits, - pipeline, + _pipeline, layoutEngine, baseFontSize: 16f, qrSvgProvider: new QrSvgProvider(), @@ -134,14 +135,15 @@ protected static Template CreateTemplate(int width, int height) /// /// Thrown when is empty or whitespace. /// - protected void AssertSvgSnapshot(string testName, Template template, ObjectValue data) + protected async Task AssertSvgSnapshot(string testName, Template template, ObjectValue data) { ArgumentNullException.ThrowIfNull(testName); ArgumentNullException.ThrowIfNull(template); ArgumentNullException.ThrowIfNull(data); ArgumentException.ThrowIfNullOrWhiteSpace(testName); - var actualSvg = _engine.RenderToSvg(template, data); + var processedTemplate = await _pipeline.ProcessAsync(template, data); + var actualSvg = _engine.RenderToSvgFromProcessed(processedTemplate); if (_updateSnapshots) { diff --git a/tests/FlexRender.Tests/Snapshots/SvgSnapshotTests.cs b/tests/FlexRender.Tests/Snapshots/SvgSnapshotTests.cs index cca224f..f4e4c40 100644 --- a/tests/FlexRender.Tests/Snapshots/SvgSnapshotTests.cs +++ b/tests/FlexRender.Tests/Snapshots/SvgSnapshotTests.cs @@ -25,7 +25,7 @@ public sealed class SvgSnapshotTests : SvgSnapshotTestBase /// with the expected font-size, fill color, and text-anchor attributes. /// [Fact] - public void SvgTextBasic() + public async Task SvgTextBasic() { var template = CreateTemplate(300, 100); template.AddElement(new TextElement @@ -37,7 +37,7 @@ public void SvgTextBasic() Width = "300" }); - AssertSvgSnapshot("svg_text_basic", template, new ObjectValue()); + await AssertSvgSnapshot("svg_text_basic", template, new ObjectValue()); } /// @@ -46,7 +46,7 @@ public void SvgTextBasic() /// with the correct stroke-dasharray attribute. /// [Fact] - public void SvgSeparatorHorizontal() + public async Task SvgSeparatorHorizontal() { var template = CreateTemplate(300, 50); var flex = new FlexElement @@ -66,7 +66,7 @@ public void SvgSeparatorHorizontal() template.AddElement(flex); - AssertSvgSnapshot("svg_separator_horizontal", template, new ObjectValue()); + await AssertSvgSnapshot("svg_separator_horizontal", template, new ObjectValue()); } /// @@ -75,7 +75,7 @@ public void SvgSeparatorHorizontal() /// for each child element. /// [Fact] - public void SvgFlexColumn() + public async Task SvgFlexColumn() { var template = CreateTemplate(300, 200); var flex = new FlexElement @@ -91,7 +91,7 @@ public void SvgFlexColumn() template.AddElement(flex); - AssertSvgSnapshot("svg_flex_column", template, new ObjectValue()); + await AssertSvgSnapshot("svg_flex_column", template, new ObjectValue()); } /// @@ -100,7 +100,7 @@ public void SvgFlexColumn() /// with children placed side by side. /// [Fact] - public void SvgFlexRow() + public async Task SvgFlexRow() { var template = CreateTemplate(300, 100); var flex = new FlexElement @@ -115,7 +115,7 @@ public void SvgFlexRow() template.AddElement(flex); - AssertSvgSnapshot("svg_flex_row", template, new ObjectValue()); + await AssertSvgSnapshot("svg_flex_row", template, new ObjectValue()); } /// @@ -124,7 +124,7 @@ public void SvgFlexRow() /// for each nesting level and that positioning is accurate. /// [Fact] - public void SvgBackgroundColors() + public async Task SvgBackgroundColors() { var template = CreateTemplate(300, 200); @@ -157,7 +157,7 @@ public void SvgBackgroundColors() outer.AddChild(inner); template.AddElement(outer); - AssertSvgSnapshot("svg_background_colors", template, new ObjectValue()); + await AssertSvgSnapshot("svg_background_colors", template, new ObjectValue()); } /// @@ -166,7 +166,7 @@ public void SvgBackgroundColors() /// as an SVG rect with fill="none" and appropriate stroke attributes. /// [Fact] - public void SvgBorderBasic() + public async Task SvgBorderBasic() { var template = CreateTemplate(300, 150); @@ -188,7 +188,7 @@ public void SvgBorderBasic() template.AddElement(box); - AssertSvgSnapshot("svg_border_basic", template, new ObjectValue()); + await AssertSvgSnapshot("svg_border_basic", template, new ObjectValue()); } /// @@ -197,7 +197,7 @@ public void SvgBorderBasic() /// rather than rasterized base64 images. /// [Fact] - public void SvgQrBasic() + public async Task SvgQrBasic() { var template = CreateTemplate(200, 200); @@ -218,7 +218,7 @@ public void SvgQrBasic() template.AddElement(flex); - AssertSvgSnapshot("svg_qr_basic", template, new ObjectValue()); + await AssertSvgSnapshot("svg_qr_basic", template, new ObjectValue()); } /// @@ -227,7 +227,7 @@ public void SvgQrBasic() /// for Code 128 barcodes with correct foreground and background fills. /// [Fact] - public void SvgBarcodeBasic() + public async Task SvgBarcodeBasic() { var template = CreateTemplate(300, 120); @@ -249,6 +249,6 @@ public void SvgBarcodeBasic() template.AddElement(flex); - AssertSvgSnapshot("svg_barcode_basic", template, new ObjectValue()); + await AssertSvgSnapshot("svg_barcode_basic", template, new ObjectValue()); } } diff --git a/tests/FlexRender.Tests/Snapshots/VisualSnapshotTests.cs b/tests/FlexRender.Tests/Snapshots/VisualSnapshotTests.cs index 4b1ae39..7705318 100644 --- a/tests/FlexRender.Tests/Snapshots/VisualSnapshotTests.cs +++ b/tests/FlexRender.Tests/Snapshots/VisualSnapshotTests.cs @@ -34,7 +34,7 @@ public sealed class VisualSnapshotTests : SnapshotTestBase /// Tests basic text rendering with default settings. /// [Fact] - public void TextSimple() + public async Task TextSimple() { if (!OperatingSystem.IsMacOS()) return; @@ -46,14 +46,14 @@ public void TextSimple() Color = "#000000" }); - AssertSnapshot("text_simple", template, new ObjectValue()); + await AssertSnapshot("text_simple", template, new ObjectValue()); } /// /// Tests styled text with bold font, red color, and center alignment. /// [Fact] - public void TextStyled() + public async Task TextStyled() { if (!OperatingSystem.IsMacOS()) return; @@ -67,14 +67,14 @@ public void TextStyled() Font = "bold" }); - AssertSnapshot("text_styled", template, new ObjectValue()); + await AssertSnapshot("text_styled", template, new ObjectValue()); } /// /// Tests multiline text with maxLines constraint and ellipsis overflow. /// [Fact] - public void TextMultiline() + public async Task TextMultiline() { if (!OperatingSystem.IsMacOS()) return; @@ -90,14 +90,14 @@ public void TextMultiline() Width = "280" }); - AssertSnapshot("text_multiline", template, new ObjectValue()); + await AssertSnapshot("text_multiline", template, new ObjectValue()); } /// /// Tests template variable substitution in text content. /// [Fact] - public void TextVariables() + public async Task TextVariables() { if (!OperatingSystem.IsMacOS()) return; @@ -114,7 +114,7 @@ public void TextVariables() ["name"] = new StringValue("World") }; - AssertSnapshot("text_variables", template, data); + await AssertSnapshot("text_variables", template, data); } #endregion @@ -125,7 +125,7 @@ public void TextVariables() /// Tests basic QR code generation. /// [Fact] - public void QrBasic() + public async Task QrBasic() { var template = CreateTemplate(300, 200); template.AddElement(new QrElement @@ -134,14 +134,14 @@ public void QrBasic() Size = 100 }); - AssertSnapshot("qr_basic", template, new ObjectValue()); + await AssertSnapshot("qr_basic", template, new ObjectValue()); } /// /// Tests QR code with custom foreground and background colors. /// [Fact] - public void QrStyled() + public async Task QrStyled() { var template = CreateTemplate(300, 200); template.AddElement(new QrElement @@ -152,14 +152,14 @@ public void QrStyled() Background = "#f0f0f0" }); - AssertSnapshot("qr_styled", template, new ObjectValue()); + await AssertSnapshot("qr_styled", template, new ObjectValue()); } /// /// Tests Code128 barcode generation with visible text. /// [Fact] - public void BarcodeCode128() + public async Task BarcodeCode128() { var template = CreateTemplate(300, 200); template.AddElement(new BarcodeElement @@ -171,7 +171,7 @@ public void BarcodeCode128() ShowText = true }); - AssertSnapshot("barcode_code128", template, new ObjectValue()); + await AssertSnapshot("barcode_code128", template, new ObjectValue()); } #endregion @@ -182,7 +182,7 @@ public void BarcodeCode128() /// Tests image rendering with contain fit mode. /// [Fact] - public void ImageContain() + public async Task ImageContain() { var imageData = CreateTestImageBase64(100, 60, SKColors.Blue); @@ -195,14 +195,14 @@ public void ImageContain() Fit = ImageFit.Contain }); - AssertSnapshot("image_contain", template, new ObjectValue()); + await AssertSnapshot("image_contain", template, new ObjectValue()); } /// /// Tests image rendering with cover fit mode. /// [Fact] - public void ImageCover() + public async Task ImageCover() { var imageData = CreateTestImageBase64(100, 60, SKColors.Green); @@ -215,7 +215,7 @@ public void ImageCover() Fit = ImageFit.Cover }); - AssertSnapshot("image_cover", template, new ObjectValue()); + await AssertSnapshot("image_cover", template, new ObjectValue()); } #endregion @@ -226,7 +226,7 @@ public void ImageCover() /// Tests vertical flex column layout with gap. /// [Fact] - public void FlexColumn() + public async Task FlexColumn() { var template = CreateTemplate(300, 200); var flex = new FlexElement @@ -240,14 +240,14 @@ public void FlexColumn() template.AddElement(flex); - AssertSnapshot("flex_column", template, new ObjectValue()); + await AssertSnapshot("flex_column", template, new ObjectValue()); } /// /// Tests horizontal flex row layout with space-between justification. /// [Fact] - public void FlexRow() + public async Task FlexRow() { if (!OperatingSystem.IsMacOS()) return; @@ -264,14 +264,14 @@ public void FlexRow() template.AddElement(flex); - AssertSnapshot("flex_row", template, new ObjectValue()); + await AssertSnapshot("flex_row", template, new ObjectValue()); } /// /// Tests two levels of nested flex containers (row inside column). /// [Fact] - public void FlexNested2Levels() + public async Task FlexNested2Levels() { var template = CreateTemplate(300, 200); @@ -294,14 +294,14 @@ public void FlexNested2Levels() template.AddElement(outerColumn); - AssertSnapshot("flex_nested_2levels", template, new ObjectValue()); + await AssertSnapshot("flex_nested_2levels", template, new ObjectValue()); } /// /// Tests three levels of nested flex containers. /// [Fact] - public void FlexNested3Levels() + public async Task FlexNested3Levels() { var template = CreateTemplate(300, 200); @@ -332,14 +332,14 @@ public void FlexNested3Levels() template.AddElement(outer); - AssertSnapshot("flex_nested_3levels", template, new ObjectValue()); + await AssertSnapshot("flex_nested_3levels", template, new ObjectValue()); } /// /// Tests flex grow distribution among child elements (1:2:1 ratio). /// [Fact] - public void FlexGrowDistribute() + public async Task FlexGrowDistribute() { var template = CreateTemplate(300, 200); var flex = new FlexElement @@ -353,14 +353,14 @@ public void FlexGrowDistribute() template.AddElement(flex); - AssertSnapshot("flex_grow_distribute", template, new ObjectValue()); + await AssertSnapshot("flex_grow_distribute", template, new ObjectValue()); } /// /// Tests cross-axis alignment with center alignment. /// [Fact] - public void FlexAlignItems() + public async Task FlexAlignItems() { var template = CreateTemplate(300, 200); var flex = new FlexElement @@ -376,14 +376,14 @@ public void FlexAlignItems() template.AddElement(flex); - AssertSnapshot("flex_align_items", template, new ObjectValue()); + await AssertSnapshot("flex_align_items", template, new ObjectValue()); } /// /// Tests space-around justification on main axis. /// [Fact] - public void FlexJustifyAll() + public async Task FlexJustifyAll() { var template = CreateTemplate(300, 200); var flex = new FlexElement @@ -398,14 +398,14 @@ public void FlexJustifyAll() template.AddElement(flex); - AssertSnapshot("flex_justify_all", template, new ObjectValue()); + await AssertSnapshot("flex_justify_all", template, new ObjectValue()); } /// /// Tests align: start with boxes of different heights aligned to the top of the container. /// [Fact] - public void FlexAlignStart() + public async Task FlexAlignStart() { if (!OperatingSystem.IsMacOS()) return; @@ -458,14 +458,14 @@ public void FlexAlignStart() flex.AddChild(box3); template.AddElement(flex); - AssertSnapshot("flex_align_start", template, new ObjectValue()); + await AssertSnapshot("flex_align_start", template, new ObjectValue()); } /// /// Tests align: center with boxes of different heights centered vertically in the container. /// [Fact] - public void FlexAlignCenter() + public async Task FlexAlignCenter() { if (!OperatingSystem.IsMacOS()) return; @@ -518,14 +518,14 @@ public void FlexAlignCenter() flex.AddChild(box3); template.AddElement(flex); - AssertSnapshot("flex_align_center", template, new ObjectValue()); + await AssertSnapshot("flex_align_center", template, new ObjectValue()); } /// /// Tests align: end with boxes of different heights aligned to the bottom of the container. /// [Fact] - public void FlexAlignEnd() + public async Task FlexAlignEnd() { if (!OperatingSystem.IsMacOS()) return; @@ -578,7 +578,7 @@ public void FlexAlignEnd() flex.AddChild(box3); template.AddElement(flex); - AssertSnapshot("flex_align_end", template, new ObjectValue()); + await AssertSnapshot("flex_align_end", template, new ObjectValue()); } /// @@ -586,7 +586,7 @@ public void FlexAlignEnd() /// Child boxes have no explicit height so they stretch to the container's 140px height. /// [Fact] - public void FlexAlignStretch() + public async Task FlexAlignStretch() { if (!OperatingSystem.IsMacOS()) return; @@ -636,7 +636,7 @@ public void FlexAlignStretch() flex.AddChild(box3); template.AddElement(flex); - AssertSnapshot("flex_align_stretch", template, new ObjectValue()); + await AssertSnapshot("flex_align_stretch", template, new ObjectValue()); } /// @@ -644,7 +644,7 @@ public void FlexAlignStretch() /// aligned along their text baselines. /// [Fact] - public void FlexAlignBaseline() + public async Task FlexAlignBaseline() { if (!OperatingSystem.IsMacOS()) return; @@ -694,14 +694,14 @@ public void FlexAlignBaseline() flex.AddChild(box3); template.AddElement(flex); - AssertSnapshot("flex_align_baseline", template, new ObjectValue()); + await AssertSnapshot("flex_align_baseline", template, new ObjectValue()); } /// /// Tests justify: start with boxes packed toward the start of the main axis. /// [Fact] - public void FlexJustifyStart() + public async Task FlexJustifyStart() { if (!OperatingSystem.IsMacOS()) return; @@ -754,14 +754,14 @@ public void FlexJustifyStart() flex.AddChild(box3); template.AddElement(flex); - AssertSnapshot("flex_justify_start", template, new ObjectValue()); + await AssertSnapshot("flex_justify_start", template, new ObjectValue()); } /// /// Tests justify: center with boxes centered along the main axis. /// [Fact] - public void FlexJustifyCenter() + public async Task FlexJustifyCenter() { if (!OperatingSystem.IsMacOS()) return; @@ -814,14 +814,14 @@ public void FlexJustifyCenter() flex.AddChild(box3); template.AddElement(flex); - AssertSnapshot("flex_justify_center", template, new ObjectValue()); + await AssertSnapshot("flex_justify_center", template, new ObjectValue()); } /// /// Tests justify: end with boxes packed toward the end of the main axis. /// [Fact] - public void FlexJustifyEnd() + public async Task FlexJustifyEnd() { if (!OperatingSystem.IsMacOS()) return; @@ -874,7 +874,7 @@ public void FlexJustifyEnd() flex.AddChild(box3); template.AddElement(flex); - AssertSnapshot("flex_justify_end", template, new ObjectValue()); + await AssertSnapshot("flex_justify_end", template, new ObjectValue()); } /// @@ -882,7 +882,7 @@ public void FlexJustifyEnd() /// First item at start, last item at end, remaining space distributed evenly. /// [Fact] - public void FlexJustifySpaceBetween() + public async Task FlexJustifySpaceBetween() { if (!OperatingSystem.IsMacOS()) return; @@ -934,7 +934,7 @@ public void FlexJustifySpaceBetween() flex.AddChild(box3); template.AddElement(flex); - AssertSnapshot("flex_justify_space_between", template, new ObjectValue()); + await AssertSnapshot("flex_justify_space_between", template, new ObjectValue()); } /// @@ -942,7 +942,7 @@ public void FlexJustifySpaceBetween() /// The space before the first item, between each item, and after the last item are all equal. /// [Fact] - public void FlexJustifySpaceEvenly() + public async Task FlexJustifySpaceEvenly() { if (!OperatingSystem.IsMacOS()) return; @@ -994,7 +994,7 @@ public void FlexJustifySpaceEvenly() flex.AddChild(box3); template.AddElement(flex); - AssertSnapshot("flex_justify_space_evenly", template, new ObjectValue()); + await AssertSnapshot("flex_justify_space_evenly", template, new ObjectValue()); } /// @@ -1003,7 +1003,7 @@ public void FlexJustifySpaceEvenly() /// space for justify-content to distribute. /// [Fact] - public void FlexRowJustifyWithFlexChildren() + public async Task FlexRowJustifyWithFlexChildren() { var template = CreateTemplate(400, 200); var flex = new FlexElement @@ -1049,7 +1049,7 @@ public void FlexRowJustifyWithFlexChildren() flex.AddChild(child3); template.AddElement(flex); - AssertSnapshot("flex_row_justify_with_flex_children", template, new ObjectValue()); + await AssertSnapshot("flex_row_justify_with_flex_children", template, new ObjectValue()); } /// @@ -1057,7 +1057,7 @@ public void FlexRowJustifyWithFlexChildren() /// produce negative child positions when content exceeds available space. /// [Fact] - public void FlexColumnAutoHeightJustifyCenter() + public async Task FlexColumnAutoHeightJustifyCenter() { var template = CreateTemplate(300, 200); var row = new FlexElement @@ -1087,14 +1087,14 @@ public void FlexColumnAutoHeightJustifyCenter() row.AddChild(column); template.AddElement(row); - AssertSnapshot("flex_column_autoheight_justify_center", template, new ObjectValue()); + await AssertSnapshot("flex_column_autoheight_justify_center", template, new ObjectValue()); } /// /// Tests mixed content types (text, QR, barcode) in a single flex row. /// [Fact] - public void FlexMixedContent() + public async Task FlexMixedContent() { if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return; @@ -1112,14 +1112,14 @@ public void FlexMixedContent() template.AddElement(flex); - AssertSnapshot("flex_mixed_content", template, new ObjectValue()); + await AssertSnapshot("flex_mixed_content", template, new ObjectValue()); } /// /// Tests percentage-based width distribution (30% and 70%). /// [Fact] - public void FlexPercentWidths() + public async Task FlexPercentWidths() { var template = CreateTemplate(300, 200); var flex = new FlexElement @@ -1132,14 +1132,14 @@ public void FlexPercentWidths() template.AddElement(flex); - AssertSnapshot("flex_percent_widths", template, new ObjectValue()); + await AssertSnapshot("flex_percent_widths", template, new ObjectValue()); } /// /// Tests combination of padding and gap with multiple children. /// [Fact] - public void FlexPaddingGapCombo() + public async Task FlexPaddingGapCombo() { var template = CreateTemplate(300, 200); var flex = new FlexElement @@ -1156,7 +1156,7 @@ public void FlexPaddingGapCombo() template.AddElement(flex); - AssertSnapshot("flex_padding_gap_combo", template, new ObjectValue()); + await AssertSnapshot("flex_padding_gap_combo", template, new ObjectValue()); } #endregion @@ -1167,7 +1167,7 @@ public void FlexPaddingGapCombo() /// Tests flex container with a solid red background color. /// [Fact] - public void FlexWithBackground() + public async Task FlexWithBackground() { var template = CreateTemplate(300, 200); var flex = new FlexElement @@ -1181,14 +1181,14 @@ public void FlexWithBackground() template.AddElement(flex); - AssertSnapshot("flex_with_background", template, new ObjectValue()); + await AssertSnapshot("flex_with_background", template, new ObjectValue()); } /// /// Tests text element with a solid blue background color. /// [Fact] - public void TextWithBackground() + public async Task TextWithBackground() { if (!OperatingSystem.IsMacOS()) return; @@ -1201,7 +1201,7 @@ public void TextWithBackground() Background = "#0000ff" }); - AssertSnapshot("text_with_background", template, new ObjectValue()); + await AssertSnapshot("text_with_background", template, new ObjectValue()); } /// @@ -1209,7 +1209,7 @@ public void TextWithBackground() /// Outer container has gray background, inner has light blue, and text has yellow. /// [Fact] - public void NestedBackgrounds() + public async Task NestedBackgrounds() { var template = CreateTemplate(300, 200); @@ -1242,14 +1242,14 @@ public void NestedBackgrounds() outer.AddChild(inner); template.AddElement(outer); - AssertSnapshot("nested_backgrounds", template, new ObjectValue()); + await AssertSnapshot("nested_backgrounds", template, new ObjectValue()); } /// /// Tests flex container with padding around child text element. /// [Fact] - public void FlexWithPadding() + public async Task FlexWithPadding() { var template = CreateTemplate(300, 200); var flex = new FlexElement @@ -1263,14 +1263,14 @@ public void FlexWithPadding() template.AddElement(flex); - AssertSnapshot("flex_with_padding", template, new ObjectValue()); + await AssertSnapshot("flex_with_padding", template, new ObjectValue()); } /// /// Tests two flex containers with margin creating spacing between them. /// [Fact] - public void FlexWithMargin() + public async Task FlexWithMargin() { var template = CreateTemplate(300, 200); @@ -1303,7 +1303,7 @@ public void FlexWithMargin() container.AddChild(second); template.AddElement(container); - AssertSnapshot("flex_with_margin", template, new ObjectValue()); + await AssertSnapshot("flex_with_margin", template, new ObjectValue()); } /// @@ -1311,7 +1311,7 @@ public void FlexWithMargin() /// that padding creates space inside the background area. /// [Fact] - public void BackgroundWithPadding() + public async Task BackgroundWithPadding() { var template = CreateTemplate(300, 200); var flex = new FlexElement @@ -1326,7 +1326,7 @@ public void BackgroundWithPadding() template.AddElement(flex); - AssertSnapshot("background_with_padding", template, new ObjectValue()); + await AssertSnapshot("background_with_padding", template, new ObjectValue()); } /// @@ -1334,7 +1334,7 @@ public void BackgroundWithPadding() /// margin creates space outside the background area. /// [Fact] - public void ElementWithMarginAndBackground() + public async Task ElementWithMarginAndBackground() { var template = CreateTemplate(300, 200); @@ -1360,7 +1360,7 @@ public void ElementWithMarginAndBackground() container.AddChild(child); template.AddElement(container); - AssertSnapshot("element_with_margin_and_background", template, new ObjectValue()); + await AssertSnapshot("element_with_margin_and_background", template, new ObjectValue()); } #endregion @@ -1372,7 +1372,7 @@ public void ElementWithMarginAndBackground() /// Blue box appears on the right, red in the middle, green on the left. /// [Fact] - public void RtlRowLayout() + public async Task RtlRowLayout() { if (!OperatingSystem.IsMacOS()) return; @@ -1411,7 +1411,7 @@ public void RtlRowLayout() flex.AddChild(box3); template.AddElement(flex); - AssertSnapshot("rtl_row_layout", template, new ObjectValue()); + await AssertSnapshot("rtl_row_layout", template, new ObjectValue()); } /// @@ -1419,7 +1419,7 @@ public void RtlRowLayout() /// "First" appears on the right, "Second" in the middle, "Third" on the left. /// [Fact] - public void RtlRowWithText() + public async Task RtlRowWithText() { if (!OperatingSystem.IsMacOS()) return; @@ -1438,7 +1438,7 @@ public void RtlRowWithText() template.AddElement(flex); - AssertSnapshot("rtl_row_with_text", template, new ObjectValue()); + await AssertSnapshot("rtl_row_with_text", template, new ObjectValue()); } /// @@ -1446,7 +1446,7 @@ public void RtlRowWithText() /// The text "Start Aligned" should appear right-aligned within the 300px canvas. /// [Fact] - public void RtlTextAlignStart() + public async Task RtlTextAlignStart() { if (!OperatingSystem.IsMacOS()) return; @@ -1462,7 +1462,7 @@ public void RtlTextAlignStart() Width = "300" }); - AssertSnapshot("rtl_text_align_start", template, new ObjectValue()); + await AssertSnapshot("rtl_text_align_start", template, new ObjectValue()); } #endregion diff --git a/tests/FlexRender.Tests/Snapshots/WbReceiptSnapshotTests.cs b/tests/FlexRender.Tests/Snapshots/WbReceiptSnapshotTests.cs index d955a83..a69fb0c 100644 --- a/tests/FlexRender.Tests/Snapshots/WbReceiptSnapshotTests.cs +++ b/tests/FlexRender.Tests/Snapshots/WbReceiptSnapshotTests.cs @@ -34,7 +34,7 @@ public sealed class WbReceiptSnapshotTests : SnapshotTestBase /// that the output matches the golden snapshot image. /// [Fact] - public void WbReceipt_RendersCorrectly() + public async Task WbReceipt_RendersCorrectly() { var repoRoot = FindRepositoryRoot(); var yamlPath = Path.Combine(repoRoot, "examples", "private", "wb-receipt.yaml"); @@ -56,7 +56,7 @@ public void WbReceipt_RendersCorrectly() try { Directory.SetCurrentDirectory(Path.Combine(repoRoot, "examples")); - AssertSnapshot("wb_receipt", template, data, colorThreshold: 10); + await AssertSnapshot("wb_receipt", template, data, colorThreshold: 10); } finally { diff --git a/tests/FlexRender.Tests/Table/TableExpansionTests.cs b/tests/FlexRender.Tests/Table/TableExpansionTests.cs index 2ed174a..980aa4a 100644 --- a/tests/FlexRender.Tests/Table/TableExpansionTests.cs +++ b/tests/FlexRender.Tests/Table/TableExpansionTests.cs @@ -41,7 +41,7 @@ private static TableElement CreateDynamicTable( // === Dynamic table expansion === [Fact] - public void Expand_DynamicTable_CreatesFlexContainerWithRows() + public async Task Expand_DynamicTable_CreatesFlexContainerWithRows() { // Arrange var columns = new List @@ -62,7 +62,7 @@ public void Expand_DynamicTable_CreatesFlexContainerWithRows() }; // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: should produce a column FlexElement wrapping rows Assert.Single(result.Elements); @@ -73,7 +73,7 @@ public void Expand_DynamicTable_CreatesFlexContainerWithRows() } [Fact] - public void Expand_DynamicTableWithHeaders_CreatesHeaderRowAndDataRows() + public async Task Expand_DynamicTableWithHeaders_CreatesHeaderRowAndDataRows() { // Arrange var columns = new List @@ -93,7 +93,7 @@ public void Expand_DynamicTableWithHeaders_CreatesHeaderRowAndDataRows() }; // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: should have header row + data rows Assert.Single(result.Elements); @@ -115,7 +115,7 @@ public void Expand_DynamicTableWithHeaders_CreatesHeaderRowAndDataRows() } [Fact] - public void Expand_DynamicTable_DataRowsHaveCorrectContent() + public async Task Expand_DynamicTable_DataRowsHaveCorrectContent() { // Arrange var columns = new List @@ -135,7 +135,7 @@ public void Expand_DynamicTable_DataRowsHaveCorrectContent() }; // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: the data row should contain TextElements with substituted values var outerFlex = Assert.IsType(result.Elements[0]); @@ -149,7 +149,7 @@ public void Expand_DynamicTable_DataRowsHaveCorrectContent() } [Fact] - public void Expand_DynamicTableWithFormat_UsesFormatString() + public async Task Expand_DynamicTableWithFormat_UsesFormatString() { // Arrange var columns = new List @@ -169,7 +169,7 @@ public void Expand_DynamicTableWithFormat_UsesFormatString() }; // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: price column should use format string var outerFlex = Assert.IsType(result.Elements[0]); @@ -179,7 +179,7 @@ public void Expand_DynamicTableWithFormat_UsesFormatString() } [Fact] - public void Expand_DynamicTableWithAsVariable_SetsItemVariable() + public async Task Expand_DynamicTableWithAsVariable_SetsItemVariable() { // Arrange var columns = new List @@ -198,7 +198,7 @@ public void Expand_DynamicTableWithAsVariable_SetsItemVariable() }; // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: should expand correctly with 'as' variable Assert.Single(result.Elements); @@ -209,7 +209,7 @@ public void Expand_DynamicTableWithAsVariable_SetsItemVariable() // === Static table expansion === [Fact] - public void Expand_StaticTableWithRows_CreatesFlexContainerWithRows() + public async Task Expand_StaticTableWithRows_CreatesFlexContainerWithRows() { // Arrange var columns = new List @@ -228,7 +228,7 @@ public void Expand_StaticTableWithRows_CreatesFlexContainerWithRows() var data = new ObjectValue(); // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert Assert.Single(result.Elements); @@ -248,7 +248,7 @@ public void Expand_StaticTableWithRows_CreatesFlexContainerWithRows() // === Column property propagation === [Fact] - public void Expand_ColumnWidth_AppliedToEachCell() + public async Task Expand_ColumnWidth_AppliedToEachCell() { // Arrange var columns = new List @@ -266,7 +266,7 @@ public void Expand_ColumnWidth_AppliedToEachCell() var data = new ObjectValue(); // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: the second column cell should have width "60" var outerFlex = Assert.IsType(result.Elements[0]); @@ -276,7 +276,7 @@ public void Expand_ColumnWidth_AppliedToEachCell() } [Fact] - public void Expand_ColumnGrow_AppliedToCell() + public async Task Expand_ColumnGrow_AppliedToCell() { // Arrange var columns = new List @@ -293,7 +293,7 @@ public void Expand_ColumnGrow_AppliedToCell() var data = new ObjectValue(); // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: the cell should have grow=1 var outerFlex = Assert.IsType(result.Elements[0]); @@ -303,7 +303,7 @@ public void Expand_ColumnGrow_AppliedToCell() } [Fact] - public void Expand_ColumnAlign_AppliedToTextElement() + public async Task Expand_ColumnAlign_AppliedToTextElement() { // Arrange var columns = new List @@ -320,7 +320,7 @@ public void Expand_ColumnAlign_AppliedToTextElement() var data = new ObjectValue(); // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert var outerFlex = Assert.IsType(result.Elements[0]); @@ -330,7 +330,7 @@ public void Expand_ColumnAlign_AppliedToTextElement() } [Fact] - public void Expand_ColumnFontAndColor_AppliedToCell() + public async Task Expand_ColumnFontAndColor_AppliedToCell() { // Arrange var columns = new List @@ -347,7 +347,7 @@ public void Expand_ColumnFontAndColor_AppliedToCell() var data = new ObjectValue(); // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert var outerFlex = Assert.IsType(result.Elements[0]); @@ -361,7 +361,7 @@ public void Expand_ColumnFontAndColor_AppliedToCell() // === Edge cases === [Fact] - public void Expand_EmptyArray_HeaderRendersNoDataRows() + public async Task Expand_EmptyArray_HeaderRendersNoDataRows() { // Arrange var columns = new List @@ -377,7 +377,7 @@ public void Expand_EmptyArray_HeaderRendersNoDataRows() }; // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: header should still render, but no data rows Assert.Single(result.Elements); @@ -394,7 +394,7 @@ public void Expand_EmptyArray_HeaderRendersNoDataRows() } [Fact] - public void Expand_MissingKeyInData_ResolvesToEmptyString() + public async Task Expand_MissingKeyInData_ResolvesToEmptyString() { // Arrange var columns = new List @@ -414,7 +414,7 @@ public void Expand_MissingKeyInData_ResolvesToEmptyString() }; // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: missing key should resolve to empty string var outerFlex = Assert.IsType(result.Elements[0]); @@ -426,7 +426,7 @@ public void Expand_MissingKeyInData_ResolvesToEmptyString() // === Spacing === [Fact] - public void Expand_TableWithGap_SetsGapOnFlexContainers() + public async Task Expand_TableWithGap_SetsGapOnFlexContainers() { // Arrange var columns = new List @@ -448,7 +448,7 @@ public void Expand_TableWithGap_SetsGapOnFlexContainers() var data = new ObjectValue(); // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: the outer flex should have rowGap, inner row flex should have columnGap var outerFlex = Assert.IsType(result.Elements[0]); @@ -461,7 +461,7 @@ public void Expand_TableWithGap_SetsGapOnFlexContainers() // === Header with border bottom (separator) === [Fact] - public void Expand_HeaderBorderBottom_CreatesSeparatorAfterHeader() + public async Task Expand_HeaderBorderBottom_CreatesSeparatorAfterHeader() { // Arrange var columns = new List @@ -484,7 +484,7 @@ public void Expand_HeaderBorderBottom_CreatesSeparatorAfterHeader() }; // Act - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); // Assert: should have header, separator, data row var outerFlex = Assert.IsType(result.Elements[0]); diff --git a/tests/FlexRender.Tests/TemplateEngine/ConditionOperatorTests.cs b/tests/FlexRender.Tests/TemplateEngine/ConditionOperatorTests.cs index fc1a7b0..36fd4bc 100644 --- a/tests/FlexRender.Tests/TemplateEngine/ConditionOperatorTests.cs +++ b/tests/FlexRender.Tests/TemplateEngine/ConditionOperatorTests.cs @@ -46,37 +46,37 @@ private static string GetResultContent(Template result) // === In Operator Tests === [Fact] - public void In_ValueInList_ReturnsTrue() + public async Task In_ValueInList_ReturnsTrue() { var ifElem = CreateIfElement("status", ConditionOperator.In, new List { "paid", "completed", "shipped" }); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["status"] = new StringValue("paid") }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void In_ValueNotInList_ReturnsFalse() + public async Task In_ValueNotInList_ReturnsFalse() { var ifElem = CreateIfElement("status", ConditionOperator.In, new List { "paid", "completed", "shipped" }); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["status"] = new StringValue("pending") }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } [Fact] - public void In_NumberValueInList_ReturnsTrue() + public async Task In_NumberValueInList_ReturnsTrue() { var ifElem = CreateIfElement("code", ConditionOperator.In, new List { "100", "200", "300" }); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["code"] = new NumberValue(200) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } @@ -84,25 +84,25 @@ public void In_NumberValueInList_ReturnsTrue() // === NotIn Operator Tests === [Fact] - public void NotIn_ValueNotInList_ReturnsTrue() + public async Task NotIn_ValueNotInList_ReturnsTrue() { var ifElem = CreateIfElement("status", ConditionOperator.NotIn, new List { "cancelled", "refunded" }); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["status"] = new StringValue("active") }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void NotIn_ValueInList_ReturnsFalse() + public async Task NotIn_ValueInList_ReturnsFalse() { var ifElem = CreateIfElement("status", ConditionOperator.NotIn, new List { "cancelled", "refunded" }); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["status"] = new StringValue("cancelled") }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } @@ -110,7 +110,7 @@ public void NotIn_ValueInList_ReturnsFalse() // === Contains Operator Tests === [Fact] - public void Contains_ArrayContainsValue_ReturnsTrue() + public async Task Contains_ArrayContainsValue_ReturnsTrue() { var ifElem = CreateIfElement("roles", ConditionOperator.Contains, "admin"); var template = CreateTemplate(ifElem); @@ -124,13 +124,13 @@ public void Contains_ArrayContainsValue_ReturnsTrue() }) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void Contains_ArrayDoesNotContain_ReturnsFalse() + public async Task Contains_ArrayDoesNotContain_ReturnsFalse() { var ifElem = CreateIfElement("roles", ConditionOperator.Contains, "superadmin"); var template = CreateTemplate(ifElem); @@ -143,13 +143,13 @@ public void Contains_ArrayDoesNotContain_ReturnsFalse() }) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } [Fact] - public void Contains_ArrayWithNumbers_ReturnsTrue() + public async Task Contains_ArrayWithNumbers_ReturnsTrue() { var ifElem = CreateIfElement("ids", ConditionOperator.Contains, "42"); var template = CreateTemplate(ifElem); @@ -163,19 +163,19 @@ public void Contains_ArrayWithNumbers_ReturnsTrue() }) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void Contains_NonArrayValue_ReturnsFalse() + public async Task Contains_NonArrayValue_ReturnsFalse() { var ifElem = CreateIfElement("name", ConditionOperator.Contains, "test"); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["name"] = new StringValue("test string") }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } @@ -183,37 +183,37 @@ public void Contains_NonArrayValue_ReturnsFalse() // === GreaterThan Operator Tests === [Fact] - public void GreaterThan_ValueGreater_ReturnsTrue() + public async Task GreaterThan_ValueGreater_ReturnsTrue() { var ifElem = CreateIfElement("amount", ConditionOperator.GreaterThan, 1000.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["amount"] = new NumberValue(1500) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void GreaterThan_ValueEqual_ReturnsFalse() + public async Task GreaterThan_ValueEqual_ReturnsFalse() { var ifElem = CreateIfElement("amount", ConditionOperator.GreaterThan, 1000.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["amount"] = new NumberValue(1000) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } [Fact] - public void GreaterThan_ValueLess_ReturnsFalse() + public async Task GreaterThan_ValueLess_ReturnsFalse() { var ifElem = CreateIfElement("amount", ConditionOperator.GreaterThan, 1000.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["amount"] = new NumberValue(500) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } @@ -221,37 +221,37 @@ public void GreaterThan_ValueLess_ReturnsFalse() // === GreaterThanOrEqual Operator Tests === [Fact] - public void GreaterThanOrEqual_ValueGreater_ReturnsTrue() + public async Task GreaterThanOrEqual_ValueGreater_ReturnsTrue() { var ifElem = CreateIfElement("amount", ConditionOperator.GreaterThanOrEqual, 1000.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["amount"] = new NumberValue(1500) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void GreaterThanOrEqual_ValueEqual_ReturnsTrue() + public async Task GreaterThanOrEqual_ValueEqual_ReturnsTrue() { var ifElem = CreateIfElement("amount", ConditionOperator.GreaterThanOrEqual, 1000.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["amount"] = new NumberValue(1000) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void GreaterThanOrEqual_ValueLess_ReturnsFalse() + public async Task GreaterThanOrEqual_ValueLess_ReturnsFalse() { var ifElem = CreateIfElement("amount", ConditionOperator.GreaterThanOrEqual, 1000.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["amount"] = new NumberValue(999) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } @@ -259,37 +259,37 @@ public void GreaterThanOrEqual_ValueLess_ReturnsFalse() // === LessThan Operator Tests === [Fact] - public void LessThan_ValueLess_ReturnsTrue() + public async Task LessThan_ValueLess_ReturnsTrue() { var ifElem = CreateIfElement("discount", ConditionOperator.LessThan, 50.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["discount"] = new NumberValue(25) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void LessThan_ValueEqual_ReturnsFalse() + public async Task LessThan_ValueEqual_ReturnsFalse() { var ifElem = CreateIfElement("discount", ConditionOperator.LessThan, 50.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["discount"] = new NumberValue(50) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } [Fact] - public void LessThan_ValueGreater_ReturnsFalse() + public async Task LessThan_ValueGreater_ReturnsFalse() { var ifElem = CreateIfElement("discount", ConditionOperator.LessThan, 50.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["discount"] = new NumberValue(75) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } @@ -297,37 +297,37 @@ public void LessThan_ValueGreater_ReturnsFalse() // === LessThanOrEqual Operator Tests === [Fact] - public void LessThanOrEqual_ValueLess_ReturnsTrue() + public async Task LessThanOrEqual_ValueLess_ReturnsTrue() { var ifElem = CreateIfElement("discount", ConditionOperator.LessThanOrEqual, 50.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["discount"] = new NumberValue(25) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void LessThanOrEqual_ValueEqual_ReturnsTrue() + public async Task LessThanOrEqual_ValueEqual_ReturnsTrue() { var ifElem = CreateIfElement("discount", ConditionOperator.LessThanOrEqual, 50.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["discount"] = new NumberValue(50) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void LessThanOrEqual_ValueGreater_ReturnsFalse() + public async Task LessThanOrEqual_ValueGreater_ReturnsFalse() { var ifElem = CreateIfElement("discount", ConditionOperator.LessThanOrEqual, 50.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["discount"] = new NumberValue(51) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } @@ -335,7 +335,7 @@ public void LessThanOrEqual_ValueGreater_ReturnsFalse() // === HasItems Operator Tests === [Fact] - public void HasItems_NonEmptyArray_ReturnsTrue() + public async Task HasItems_NonEmptyArray_ReturnsTrue() { var ifElem = CreateIfElement("items", ConditionOperator.HasItems, true); var template = CreateTemplate(ifElem); @@ -348,13 +348,13 @@ public void HasItems_NonEmptyArray_ReturnsTrue() }) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void HasItems_EmptyArray_ReturnsFalse() + public async Task HasItems_EmptyArray_ReturnsFalse() { var ifElem = CreateIfElement("items", ConditionOperator.HasItems, true); var template = CreateTemplate(ifElem); @@ -363,13 +363,13 @@ public void HasItems_EmptyArray_ReturnsFalse() ["items"] = new ArrayValue(new List()) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } [Fact] - public void HasItems_EmptyArrayExpectedFalse_ReturnsTrue() + public async Task HasItems_EmptyArrayExpectedFalse_ReturnsTrue() { var ifElem = CreateIfElement("items", ConditionOperator.HasItems, false); var template = CreateTemplate(ifElem); @@ -378,19 +378,19 @@ public void HasItems_EmptyArrayExpectedFalse_ReturnsTrue() ["items"] = new ArrayValue(new List()) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void HasItems_NonArrayValue_ReturnsFalse() + public async Task HasItems_NonArrayValue_ReturnsFalse() { var ifElem = CreateIfElement("name", ConditionOperator.HasItems, true); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["name"] = new StringValue("test") }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } @@ -398,7 +398,7 @@ public void HasItems_NonArrayValue_ReturnsFalse() // === CountEquals Operator Tests === [Fact] - public void CountEquals_CorrectCount_ReturnsTrue() + public async Task CountEquals_CorrectCount_ReturnsTrue() { var ifElem = CreateIfElement("items", ConditionOperator.CountEquals, 3); var template = CreateTemplate(ifElem); @@ -412,13 +412,13 @@ public void CountEquals_CorrectCount_ReturnsTrue() }) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void CountEquals_WrongCount_ReturnsFalse() + public async Task CountEquals_WrongCount_ReturnsFalse() { var ifElem = CreateIfElement("items", ConditionOperator.CountEquals, 5); var template = CreateTemplate(ifElem); @@ -431,13 +431,13 @@ public void CountEquals_WrongCount_ReturnsFalse() }) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } [Fact] - public void CountEquals_EmptyArrayZero_ReturnsTrue() + public async Task CountEquals_EmptyArrayZero_ReturnsTrue() { var ifElem = CreateIfElement("items", ConditionOperator.CountEquals, 0); var template = CreateTemplate(ifElem); @@ -446,7 +446,7 @@ public void CountEquals_EmptyArrayZero_ReturnsTrue() ["items"] = new ArrayValue(new List()) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } @@ -454,7 +454,7 @@ public void CountEquals_EmptyArrayZero_ReturnsTrue() // === CountGreaterThan Operator Tests === [Fact] - public void CountGreaterThan_CountGreater_ReturnsTrue() + public async Task CountGreaterThan_CountGreater_ReturnsTrue() { var ifElem = CreateIfElement("attachments", ConditionOperator.CountGreaterThan, 0); var template = CreateTemplate(ifElem); @@ -467,13 +467,13 @@ public void CountGreaterThan_CountGreater_ReturnsTrue() }) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void CountGreaterThan_CountEqual_ReturnsFalse() + public async Task CountGreaterThan_CountEqual_ReturnsFalse() { var ifElem = CreateIfElement("attachments", ConditionOperator.CountGreaterThan, 2); var template = CreateTemplate(ifElem); @@ -486,13 +486,13 @@ public void CountGreaterThan_CountEqual_ReturnsFalse() }) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } [Fact] - public void CountGreaterThan_CountLess_ReturnsFalse() + public async Task CountGreaterThan_CountLess_ReturnsFalse() { var ifElem = CreateIfElement("attachments", ConditionOperator.CountGreaterThan, 5); var template = CreateTemplate(ifElem); @@ -504,7 +504,7 @@ public void CountGreaterThan_CountLess_ReturnsFalse() }) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } @@ -512,79 +512,79 @@ public void CountGreaterThan_CountLess_ReturnsFalse() // === Equals Operator Tests (Extended) === [Fact] - public void Equals_Numbers_ReturnsTrue() + public async Task Equals_Numbers_ReturnsTrue() { var ifElem = CreateIfElement("quantity", ConditionOperator.Equals, 42.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["quantity"] = new NumberValue(42) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void Equals_NumbersNotEqual_ReturnsFalse() + public async Task Equals_NumbersNotEqual_ReturnsFalse() { var ifElem = CreateIfElement("quantity", ConditionOperator.Equals, 42.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["quantity"] = new NumberValue(43) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } [Fact] - public void Equals_Booleans_ReturnsTrue() + public async Task Equals_Booleans_ReturnsTrue() { var ifElem = CreateIfElement("active", ConditionOperator.Equals, true); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["active"] = new BoolValue(true) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void Equals_Null_ReturnsTrue() + public async Task Equals_Null_ReturnsTrue() { var ifElem = CreateIfElement("missing", ConditionOperator.Equals, null); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["missing"] = NullValue.Instance }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void Equals_NonNullWithNull_ReturnsFalse() + public async Task Equals_NonNullWithNull_ReturnsFalse() { var ifElem = CreateIfElement("value", ConditionOperator.Equals, null); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["value"] = new StringValue("something") }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } [Fact] - public void Equals_MissingPathEqualsNull_ReturnsTrue() + public async Task Equals_MissingPathEqualsNull_ReturnsTrue() { var ifElem = CreateIfElement("nonexistent", ConditionOperator.Equals, null); var template = CreateTemplate(ifElem); var data = new ObjectValue(); - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void Equals_Arrays_ReturnsTrue() + public async Task Equals_Arrays_ReturnsTrue() { var compareArray = new ArrayValue(new List { @@ -602,7 +602,7 @@ public void Equals_Arrays_ReturnsTrue() }) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } @@ -610,25 +610,25 @@ public void Equals_Arrays_ReturnsTrue() // === NotEquals Operator Tests (Extended) === [Fact] - public void NotEquals_DifferentValues_ReturnsTrue() + public async Task NotEquals_DifferentValues_ReturnsTrue() { var ifElem = CreateIfElement("status", ConditionOperator.NotEquals, "cancelled"); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["status"] = new StringValue("active") }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void NotEquals_SameValues_ReturnsFalse() + public async Task NotEquals_SameValues_ReturnsFalse() { var ifElem = CreateIfElement("status", ConditionOperator.NotEquals, "cancelled"); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["status"] = new StringValue("cancelled") }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } @@ -636,31 +636,31 @@ public void NotEquals_SameValues_ReturnsFalse() // === Edge Cases === [Fact] - public void NumericComparison_NonNumericValue_ReturnsFalse() + public async Task NumericComparison_NonNumericValue_ReturnsFalse() { var ifElem = CreateIfElement("name", ConditionOperator.GreaterThan, 100.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["name"] = new StringValue("test") }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } [Fact] - public void In_NullList_ReturnsFalse() + public async Task In_NullList_ReturnsFalse() { var ifElem = CreateIfElement("status", ConditionOperator.In, null); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["status"] = new StringValue("active") }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } [Fact] - public void Contains_NullElement_ReturnsFalse() + public async Task Contains_NullElement_ReturnsFalse() { var ifElem = CreateIfElement("items", ConditionOperator.Contains, null); var template = CreateTemplate(ifElem); @@ -669,43 +669,43 @@ public void Contains_NullElement_ReturnsFalse() ["items"] = new ArrayValue(new List { new StringValue("test") }) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } [Fact] - public void CountEquals_NonArrayValue_ReturnsFalse() + public async Task CountEquals_NonArrayValue_ReturnsFalse() { var ifElem = CreateIfElement("name", ConditionOperator.CountEquals, 5); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["name"] = new StringValue("test") }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("false", GetResultContent(result)); } [Fact] - public void DecimalPrecision_GreaterThan_WorksCorrectly() + public async Task DecimalPrecision_GreaterThan_WorksCorrectly() { var ifElem = CreateIfElement("price", ConditionOperator.GreaterThan, 99.99); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["price"] = new NumberValue(100.00m) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } [Fact] - public void NegativeNumbers_LessThan_WorksCorrectly() + public async Task NegativeNumbers_LessThan_WorksCorrectly() { var ifElem = CreateIfElement("temperature", ConditionOperator.LessThan, 0.0); var template = CreateTemplate(ifElem); var data = new ObjectValue { ["temperature"] = new NumberValue(-10) }; - var result = _expander.Expand(template, data); + var result = await _expander.ExpandAsync(template, data); Assert.Equal("true", GetResultContent(result)); } diff --git a/tests/FlexRender.Tests/TemplateEngine/ExprValueIntegrationTests.cs b/tests/FlexRender.Tests/TemplateEngine/ExprValueIntegrationTests.cs index b6467b7..5662579 100644 --- a/tests/FlexRender.Tests/TemplateEngine/ExprValueIntegrationTests.cs +++ b/tests/FlexRender.Tests/TemplateEngine/ExprValueIntegrationTests.cs @@ -32,11 +32,11 @@ private static TemplatePipeline CreatePipeline() /// The YAML template string. /// The data context for expression evaluation. /// The processed template with all expressions resolved. - private Template ParseAndProcess(string yaml, ObjectValue data) + private async Task