// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.IO;
using System.Linq;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Documents;
using Xunit;
namespace Microsoft.Extensions.Documents.Tests;
public partial class DocumentTests
{
[Fact]
public void StoresIdentifierAndInitialState()
{
ExtractionDocument document = new("document-id", new TextContent("stored document"));
Assert.Equal("document-id", document.Identifier);
Assert.Equal("stored document", Assert.IsType<TextContent>(document.Content).Text);
Assert.Empty(document.Sections);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void RejectsNullOrEmptyIdentifier(string? identifier)
{
Assert.ThrowsAny<ArgumentException>(() => new ExtractionDocument(identifier!));
}
[Fact]
public void LocalTextPayload_RetainsExactSuppliedValue()
{
ExtractionDocument document = new("document", new TextContent("stored markdown"));
Assert.Equal("stored markdown", Assert.IsType<TextContent>(document.Content).Text);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void NullableRootPayload_PreservesMissingVersusEmptyText(string? markdown)
{
ExtractionDocument document = new("document", markdown is null ? null : new TextContent(markdown));
Assert.Equal(markdown, (document.Content as TextContent)?.Text);
}
[Fact]
public void Sections_RetainImagePayloadWithoutStoredAncestorAggregate()
{
ExtractionDocument document = new("document", null);
ExtractionDocumentSection section = new(null);
section.Elements.Add(new ExtractionDocumentImageElement(new TextContent("stored image markdown"))
{
Source = new Uri("urn:image:semantic"),
AlternativeText = "semantic image"
});
document.Sections.Add(section);
Assert.Null(document.Content);
ExtractionDocumentImageElement image = Assert.IsType<ExtractionDocumentImageElement>(Assert.Single(document.EnumerateContent()));
Assert.Equal("stored image markdown", Assert.IsType<TextContent>(image.Content).Text);
Assert.Equal("urn:image:semantic", image.Source!.ToString());
Assert.Equal("semantic image", image.AlternativeText);
}
[Fact]
public void MultipleSections_PreserveContentOrderWithoutJoiningPayloads()
{
ExtractionDocument document = new("document", null);
ExtractionDocumentSection first = new(null);
ExtractionDocumentSection second = new(null);
first.Elements.Add(new ExtractionDocumentTextElement(new TextContent("first semantic")));
second.Elements.Add(new ExtractionDocumentTextElement(new TextContent("second semantic")));
document.Sections.Add(first);
document.Sections.Add(second);
Assert.Equal(new[] { first, second }, document.Sections);
Assert.Equal(new[] { "first semantic", "second semantic" },
document.EnumerateContent().Select(element => Assert.IsType<TextContent>(element.Content).Text));
}
[Fact]
public void Content_SameReferencePreservesPayload()
{
ExtractionDocument document = new("document", new TextContent("stored markdown"));
AIContent? payload = document.Content;
document.Content = payload;
Assert.Same(payload, document.Content);
}
[Fact]
public void Content_ChangedReferenceUpdatesPayload()
{
ExtractionDocument document = new("document", new TextContent("stored markdown"));
Assert.Equal("stored markdown", Assert.IsType<TextContent>(document.Content).Text);
document.Content = new TextContent("updated markdown");
Assert.Equal("updated markdown", Assert.IsType<TextContent>(document.Content).Text);
}
[Fact]
public void EnumerateContent_ReturnsOrdinaryAndNestedElementsInDepthFirstOrder()
{
ExtractionDocument document = new("document");
ExtractionDocumentSection first = new(null);
ExtractionDocumentTextElement firstText = new(new TextContent("first text"));
ExtractionDocumentSection nested = new(null);
ExtractionDocumentTextElement nestedText = new(new TextContent("nested text"));
ExtractionDocumentImageElement image = new(new TextContent("image"));
ExtractionDocumentTextElement extension = new(new TextContent("extension"), role: "application-extra");
ExtractionDocumentTableElement table = new(null, 1, 1);
ExtractionDocumentSection second = new(null);
ExtractionDocumentTextElement secondText = new(new TextContent("second text"));
nested.Elements.Add(nestedText);
first.Elements.Add(firstText);
first.Elements.Add(nested);
first.Elements.Add(image);
first.Elements.Add(extension);
first.Elements.Add(table);
second.Elements.Add(secondText);
document.Sections.Add(first);
document.Sections.Add(second);
ExtractionDocumentElement[] content = document.EnumerateContent().ToArray();
Assert.Equal(
new ExtractionDocumentElement[] { firstText, nestedText, image, extension, table, secondText },
content);
}
[Fact]
public void EnumerateContent_DoesNotReturnSectionNodes()
{
ExtractionDocument document = new("document");
ExtractionDocumentSection root = new(null);
ExtractionDocumentSection nested = new(null);
root.Elements.Add(nested);
nested.Elements.Add(new ExtractionDocumentTextElement(new TextContent("content")));
document.Sections.Add(root);
ExtractionDocumentElement[] content = document.EnumerateContent().ToArray();
Assert.NotEmpty(content);
Assert.All(content, element => Assert.False(element is ExtractionDocumentSection));
}
[Fact]
public void DocumentTraversal_PreservesOriginIdentityAcrossHierarchyChanges()
{
ExtractionDocument document = new("origin");
ExtractionDocumentSourcePage page7 = DocumentAnnotationTestData.PdfPage();
ExtractionDocumentSourcePage page2 = new("urn:frame:page2", 2);
ExtractionDocumentTextElement text7 = new(new TextContent("same text"));
ExtractionDocumentTextElement text2 = new(new TextContent("same text"));
ExtractionDocumentSourceReference region = new(page7, DocumentAnnotationTestData.Polygon());
text7.Sources.Add(region);
text2.Sources.Add(new ExtractionDocumentSourceReference(page2, DocumentAnnotationTestData.Polygon()));
ExtractionDocumentSection first = new(null);
ExtractionDocumentSection second = new(null);
first.Elements.Add(text7);
second.Elements.Add(text2);
document.Sections.Add(first);
document.Sections.Add(second);
document.SourcePages.Add(page7);
document.SourcePages.Add(page2);
Assert.Equal(new[] { page7, page2 }, document.SourcePages);
first.Elements.Remove(text7);
second.Elements.Insert(0, text7);
Assert.Equal(new[] { text7, text2 }, document.EnumerateContent());
Assert.Equal(new int?[] { 7, 2 }, document.SourcePages.Select(page => page.PageNumber));
Assert.Same(page7, text7.Sources[0].Page);
Assert.Equal("urn:frame:pdf-A", region.Page.Identifier);
Assert.Equal(DocumentAnnotationTestData.Polygon(), region.Polygon);
}
[Fact]
public void BlankPage_PreservesSourceFrameWithoutFabricatedSemanticContent()
{
ExtractionDocument document = new("blank");
ExtractionDocumentSourcePage page = new("urn:frame:blank-page2", 2, new ExtractionDocumentPageDimensions(200, 100),
ExtractionDocumentCoordinateUnit.Point, ExtractionDocumentCoordinateOrigin.BottomLeft);
document.SourcePages.Add(page);
document.Sections.Add(new ExtractionDocumentSection());
Assert.Null(document.Content);
Assert.Empty(document.EnumerateContent());
Assert.Same(page, Assert.Single(document.SourcePages));
Assert.Equal(2, page.PageNumber);
Assert.Equal("urn:frame:blank-page2", page.Identifier);
Assert.Equal(new ExtractionDocumentPageDimensions(200, 100), page.Dimensions);
Assert.Equal(ExtractionDocumentCoordinateUnit.Point, page.CoordinateUnit);
Assert.Equal(ExtractionDocumentCoordinateOrigin.BottomLeft, page.CoordinateOrigin);
}
[Fact]
public void DocumentNativeResult_PageInventoryPreservesDuplicatesWithoutSemanticLeakage()
{
ExtractionDocument document = new("shared-body", new TextContent("visible"));
using DocumentAnnotationTestData.CountingStream stream = new(DocumentAnnotationTestData.OriginalBytes());
using DocumentAnnotationTestData.BorrowedNative native = new();
document.RawRepresentation = native;
ExtractionDocumentSourcePage page7 = DocumentAnnotationTestData.PdfPage();
ExtractionDocumentSourcePage page2 = new("frame-2", 2);
document.SourcePages.Add(page7);
document.SourcePages.Add(page2);
document.SourcePages.Add(page7);
Assert.Equal(new[] { page7, page2, page7 }, document.SourcePages);
Assert.Same(native, document.RawRepresentation);
Assert.Equal(DocumentAnnotationTestData.OriginalBytes(), stream.ToArray());
Assert.Equal("visible", Assert.IsType<TextContent>(document.Content).Text);
Assert.Empty(document.EnumerateContent());
Assert.Equal(0, stream.DisposeCount);
Assert.Equal(0, native.DisposeCount);
}
[Fact]
public void DocumentMetadataAndSources_AreConsumableWithoutExtractionResult()
{
ExtractionDocument document = new("shared-annotations");
ExtractionDocumentSection section = new(null);
ExtractionDocumentTextElement text = new(new TextContent("visible **text**"));
section.Elements.Add(text);
document.Sections.Add(section);
var payload = new { target = text, nested = new { unknown = new object[] { 3, "x" } }, vendorFlag = "kept" };
ExtractionDocumentSourcePage page = DocumentAnnotationTestData.PdfPage();
document.SourcePages.Add(page);
document.Metadata["vendor-extra"] = payload;
text.Sources.Add(new ExtractionDocumentSourceReference(page, DocumentAnnotationTestData.Polygon()));
Assert.Same(payload, document.Metadata["vendor-extra"]);
Assert.Same(text, payload.target);
Assert.Equal(new object[] { 3, "x" }, payload.nested.unknown);
Assert.Equal("kept", payload.vendorFlag);
Assert.Same(page, Assert.Single(document.SourcePages));
Assert.Same(text, Assert.Single(document.EnumerateContent()));
Assert.Equal("visible **text**", Assert.IsType<TextContent>(text.Content).Text);
}
}