File: DocumentTableCellCollectionTests.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.Documents.Abstractions.Tests\Microsoft.Extensions.Documents.Abstractions.Tests.csproj (Microsoft.Extensions.Documents.Abstractions.Tests)
// 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.Linq;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Documents;
using Xunit;
 
namespace Microsoft.Extensions.Documents.Tests;
 
public class DocumentTableCellCollectionTests
{
    [Fact]
    public void EnumeratesNullableCellsInRowMajorOrder()
    {
        ExtractionDocumentTableElement table = new(null, 2, 3);
        ExtractionDocumentTextElement firstContent = new(new TextContent("first"));
        ExtractionDocumentTextElement lastContent = new(new TextContent("last"));
 
        table.Cells[0, 1] = new ExtractionDocumentTableCell(firstContent);
        table.Cells[1, 2] = new ExtractionDocumentTableCell(lastContent);
 
        ExtractionDocumentTableCell?[] cells = table.Cells.ToArray();
 
        Assert.Equal(6, cells.Length);
        Assert.Null(cells[0]);
        Assert.Same(firstContent, cells[1]!.Content);
        Assert.Null(cells[2]);
        Assert.Null(cells[3]);
        Assert.Null(cells[4]);
        Assert.Same(lastContent, cells[5]!.Content);
        Assert.Equal(
            new ExtractionDocumentTableCell?[] { null, cells[1], null, null, null, cells[5] },
            cells);
    }
 
    [Fact]
    public void Indexer_PreservesNonSectionElement()
    {
        ExtractionDocument document = new("document");
        ExtractionDocumentSection section = new(null);
        ExtractionDocumentTableElement table = new(null, 1, 1);
        ExtractionDocumentTextElement content = new(new TextContent("cell"));
        document.Sections.Add(section);
        section.Elements.Add(table);
 
        table.Cells[0, 0] = new ExtractionDocumentTableCell(content);
 
        ExtractionDocumentTableCell cell = table.Cells[0, 0]!;
        Assert.Same(content, cell.Content);
        Assert.Same(cell, table.Cells[0, 0]);
    }
 
    [Fact]
    public void SupportsImageCellContent()
    {
        ExtractionDocumentTableElement table = new(null, 1, 1);
        ReadOnlyMemory<byte> imageContent = new byte[] { 1, 2, 3 };
        Uri source = new("urn:table-image:opaque-1");
        ExtractionDocumentImageElement image = new(new TextContent("stored image"))
        {
            Content = new DataContent(imageContent, "image/png"),
            Source = source,
            AlternativeText = "diagram"
        };
 
        table.Cells[0, 0] = new ExtractionDocumentTableCell(image);
 
        ExtractionDocumentImageElement actual = Assert.IsType<ExtractionDocumentImageElement>(table.Cells[0, 0]!.Content);
        Assert.Same(image, actual);
        Assert.Equal(imageContent.ToArray(), Assert.IsType<DataContent>(actual.Content).Data.ToArray());
        Assert.Equal("image/png", Assert.IsType<DataContent>(actual.Content).MediaType);
        Assert.Same(source, actual.Source);
        Assert.Equal("diagram", actual.AlternativeText);
    }
 
    [Fact]
    public void ReplacesAndRemovesContent_ReleasesCellGridAssociation()
    {
        ExtractionDocument document = new("document");
        ExtractionDocumentSection section = new(null);
        ExtractionDocumentTableElement table = new(null, 1, 1);
        ExtractionDocumentTextElement oldContent = new(new TextContent("old"));
        ExtractionDocumentTextElement replacementContent = new(new TextContent("replacement"));
        document.Sections.Add(section);
        section.Elements.Add(table);
 
        table.Cells[0, 0] = new ExtractionDocumentTableCell(oldContent);
        ExtractionDocumentTableCell oldCell = table.Cells[0, 0]!;
 
        table.Cells[0, 0] = new ExtractionDocumentTableCell(replacementContent);
 
        ExtractionDocumentTableCell replacementCell = table.Cells[0, 0]!;
        Assert.NotSame(oldCell, replacementCell);
        Assert.Null(oldCell.Table);
        Assert.Same(oldContent, oldCell.Content);
        Assert.Same(replacementContent, replacementCell.Content);
 
        table.Cells[0, 0] = null;
 
        Assert.Null(table.Cells[0, 0]);
        Assert.Null(replacementCell.Table);
        Assert.Same(replacementContent, replacementCell.Content);
    }
 
    [Fact]
    public void RejectsAlreadyOwnedCell()
    {
        ExtractionDocumentTableElement firstTable = new(null, 1, 1);
        ExtractionDocumentTableElement secondTable = new(null, 1, 1);
        ExtractionDocumentTextElement content = new(new TextContent("cell"));
        ExtractionDocumentTableCell cell = new(content);
        firstTable.Cells[0, 0] = cell;
 
        Assert.Throws<InvalidOperationException>(() => secondTable.Cells[0, 0] = cell);
 
        Assert.Same(cell, firstTable.Cells[0, 0]);
        Assert.Null(secondTable.Cells[0, 0]);
        Assert.Same(content, firstTable.Cells[0, 0]!.Content);
        Assert.Equal(0, cell.RowIndex);
        Assert.Equal(0, cell.ColumnIndex);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void MixedSectionCell_PreservesOrderedLocalPayloadsSourcesAndSpans(bool useUriContent)
    {
        ExtractionDocument document = new("mixed-cell");
        ExtractionDocumentSection root = new();
        ExtractionDocumentTableElement table = new(null, 2, 3);
        root.Elements.Add(table);
        document.Sections.Add(root);
        ExtractionDocumentSourcePage page = DocumentAnnotationTestData.PdfPage();
        document.SourcePages.Add(page);
        ExtractionDocumentSourceReference cellSource = new(page, DocumentAnnotationTestData.Polygon());
        ExtractionDocumentSourceReference firstSource = new(page, [new(1.25, 2.5), new(3.75, 2.5), new(1.25, 4.5)]);
        ExtractionDocumentSourceReference imageSource = new(page, [new(5.25, 6.5), new(7.75, 6.5), new(5.25, 8.5)]);
        ExtractionDocumentSourceReference lastSource = new(page, [new(9.25, 10.5), new(11.75, 10.5), new(9.25, 12.5)]);
        TextContent paragraphA = new("Paragraph A **before** the image.");
        TextContent paragraphB = new("Paragraph B *after* the image.");
        ExtractionDocumentTextElement first = new(paragraphA, "paragraph");
        ExtractionDocumentTextElement last = new(paragraphB, "paragraph");
        byte[] bytes = [11, 22, 33, 44];
        Uri payloadUri = new("urn:mixed-cell:payload");
        Uri originalAsset = new("urn:mixed-cell:original");
        AIContent imagePayload = useUriContent
            ? new UriContent(payloadUri, "image/png")
            : new DataContent(bytes, "image/png");
        ExtractionDocumentImageElement image = new(imagePayload, "OCR 17 & 23")
        {
            Source = originalAsset,
            AlternativeText = "A diagram, not its transcription."
        };
        first.Sources.Add(firstSource);
        image.Sources.Add(imageSource);
        last.Sources.Add(lastSource);
        ExtractionDocumentSection mixed = new();
        mixed.Elements.Add(first);
        mixed.Elements.Add(image);
        mixed.Elements.Add(last);
        ExtractionDocumentTableCell cell = new(mixed, 2, 2, "rowHeader");
        cell.Sources.Add(cellSource);
 
        table.Cells[0, 1] = cell;
 
        Assert.Same(cell, table.Cells[0, 1]);
        Assert.Same(table, cell.Table);
        Assert.Equal(0, cell.RowIndex);
        Assert.Equal(1, cell.ColumnIndex);
        Assert.Equal(2, cell.RowSpan);
        Assert.Equal(2, cell.ColumnSpan);
        Assert.Equal("rowHeader", cell.Role);
        Assert.Null(table.Cells[0, 0]);
        foreach ((int row, int column) in new[] { (0, 2), (1, 1), (1, 2) })
        {
            Assert.Null(table.Cells[row, column]);
            Assert.True(table.Cells.IsCovered(row, column));
            Assert.Same(cell, table.Cells.GetCoveringCell(row, column));
        }
 
        Assert.Same(mixed, cell.Content);
        Assert.Equal(3, mixed.Elements.Count);
        Assert.Same(first, mixed.Elements[0]);
        Assert.Same(image, mixed.Elements[1]);
        Assert.Same(last, mixed.Elements[2]);
        Assert.Same(paragraphA, first.Content);
        Assert.Same(paragraphB, last.Content);
        Assert.NotSame(first, last);
        Assert.NotSame(paragraphA, paragraphB);
        Assert.Equal("Paragraph A **before** the image.", Assert.IsType<TextContent>(first.Content).Text);
        Assert.Equal("Paragraph B *after* the image.", Assert.IsType<TextContent>(last.Content).Text);
        Assert.Equal("paragraph", first.Role);
        Assert.Equal("paragraph", last.Role);
        Assert.Same(imagePayload, image.Content);
        if (useUriContent)
        {
            UriContent payload = Assert.IsType<UriContent>(image.Content);
            Assert.Same(payloadUri, payload.Uri);
            Assert.Equal("image/png", payload.MediaType);
        }
        else
        {
            DataContent payload = Assert.IsType<DataContent>(image.Content);
            Assert.Equal(new byte[] { 11, 22, 33, 44 }, payload.Data.ToArray());
            Assert.Equal("image/png", payload.MediaType);
        }
 
        Assert.Same(originalAsset, image.Source);
        Assert.Equal("OCR 17 & 23", image.Text);
        Assert.Equal("A diagram, not its transcription.", image.AlternativeText);
        Assert.Same(cellSource, Assert.Single(cell.Sources));
        Assert.Same(firstSource, Assert.Single(first.Sources));
        Assert.Same(imageSource, Assert.Single(image.Sources));
        Assert.Same(lastSource, Assert.Single(last.Sources));
        Assert.Equal(DocumentAnnotationTestData.Polygon(), cellSource.Polygon);
        Assert.Equal(new ExtractionDocumentPoint[] { new(1.25, 2.5), new(3.75, 2.5), new(1.25, 4.5) }, firstSource.Polygon);
        Assert.Equal(new ExtractionDocumentPoint[] { new(5.25, 6.5), new(7.75, 6.5), new(5.25, 8.5) }, imageSource.Polygon);
        Assert.Equal(new ExtractionDocumentPoint[] { new(9.25, 10.5), new(11.75, 10.5), new(9.25, 12.5) }, lastSource.Polygon);
        Assert.All(new[] { cellSource, firstSource, imageSource, lastSource }, source => Assert.Same(page, source.Page));
        Assert.Same(page, Assert.Single(document.SourcePages));
        Assert.Equal(new ExtractionDocumentPageDimensions(200, 100), page.Dimensions);
        Assert.Equal(ExtractionDocumentCoordinateUnit.Point, page.CoordinateUnit);
        Assert.Equal(ExtractionDocumentCoordinateOrigin.BottomLeft, page.CoordinateOrigin);
        Assert.Null(document.Content);
        Assert.Null(root.Content);
        Assert.Null(table.Content);
        Assert.Null(mixed.Content);
        Assert.Empty(root.Sources);
        Assert.Empty(table.Sources);
        Assert.Empty(mixed.Sources);
        Assert.Same(table, Assert.Single(document.EnumerateContent()));
 
        image.Text = "Corrected OCR 17 & 28";
        Assert.Equal("Corrected OCR 17 & 28", image.Text);
        Assert.Equal("A diagram, not its transcription.", image.AlternativeText);
        image.AlternativeText = "Revised caption.";
        Assert.Equal("Revised caption.", image.AlternativeText);
        Assert.Equal("Corrected OCR 17 & 28", image.Text);
        Assert.Same(imagePayload, image.Content);
        Assert.Same(originalAsset, image.Source);
        Assert.Same(paragraphA, first.Content);
        Assert.Same(paragraphB, last.Content);
        Assert.Null(mixed.Content);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void MixedSectionCell_PostAttachmentCyclesDoNotExpandPublicTraversal(bool includeInnerTable)
    {
        ExtractionDocument document = new("mixed-cycle");
        ExtractionDocumentSection root = new();
        ExtractionDocumentTableElement table = new(null, 1, 1);
        ExtractionDocumentSection mixed = new();
        ExtractionDocumentTextElement paragraph = new(new TextContent("cell-only paragraph"));
        mixed.Elements.Add(paragraph);
        ExtractionDocumentTableCell cell = new(mixed);
        table.Cells[0, 0] = cell;
        root.Elements.Add(table);
        document.Sections.Add(root);
        Assert.Same(table, Assert.Single(document.EnumerateContent()));
 
        ExtractionDocumentTableElement? innerTable = null;
        if (includeInnerTable)
        {
            innerTable = new(null, 1, 1);
            mixed.Elements.Add(innerTable);
            innerTable.Cells[0, 0] = new ExtractionDocumentTableCell(root);
        }
        else
        {
            mixed.Elements.Add(root);
        }
 
        // A cycle behind a cell boundary is outside structural traversal.
        Assert.Same(table, Assert.Single(document.EnumerateContent()));
        Assert.Same(cell, table.Cells[0, 0]);
        Assert.Same(mixed, cell.Content);
        Assert.Same(paragraph, mixed.Elements[0]);
        Assert.Equal("cell-only paragraph", Assert.IsType<TextContent>(paragraph.Content).Text);
        if (includeInnerTable)
        {
            Assert.Same(innerTable, mixed.Elements[1]);
            Assert.Same(root, innerTable!.Cells[0, 0]!.Content);
        }
        else
        {
            Assert.Same(root, mixed.Elements[1]);
        }
 
        Assert.Null(document.Content);
        Assert.Null(table.Content);
        Assert.Null(mixed.Content);
    }
 
    [Fact]
    public void RejectsTableSelfContent()
    {
        ExtractionDocumentTableElement table = new(null, 1, 1);
 
        Assert.Throws<InvalidOperationException>(() => table.Cells[0, 0] = new ExtractionDocumentTableCell(table));
 
        Assert.Null(table.Cells[0, 0]);
    }
 
    [Fact]
    public void IndirectTableCycles_DoNotCauseStructuralTraversalToRecurseIntoCells()
    {
        ExtractionDocumentTableElement outerTable = new(null, 1, 1);
        ExtractionDocumentTableElement innerTable = new(null, 1, 1);
 
        outerTable.Cells[0, 0] = new ExtractionDocumentTableCell(innerTable);
 
        innerTable.Cells[0, 0] = new ExtractionDocumentTableCell(outerTable);
        ExtractionDocument document = new("cycle");
        ExtractionDocumentSection section = new();
        section.Elements.Add(outerTable);
        document.Sections.Add(section);
        Assert.Same(outerTable, Assert.Single(document.EnumerateContent()));
 
        Assert.Same(innerTable, outerTable.Cells[0, 0]!.Content);
        Assert.Same(outerTable, innerTable.Cells[0, 0]!.Content);
    }
 
    [Fact]
    public void SameCellAssignment_PreservesGridAndPayload()
    {
        ExtractionDocument document = new("document");
        ExtractionDocumentSection section = new(null);
        ExtractionDocumentTableElement table = new(null, 1, 1);
        ExtractionDocumentTextElement content = new(new TextContent("content"));
        document.Sections.Add(section);
        section.Elements.Add(table);
 
        table.Cells[0, 0] = new ExtractionDocumentTableCell(content);
        ExtractionDocumentTableCell cell = table.Cells[0, 0]!;
 
        table.Cells[0, 0] = cell;
 
        Assert.Same(cell, table.Cells[0, 0]);
        Assert.Same(content, cell.Content);
 
        table.Cells[0, 0] = null;
        table.Cells[0, 0] = null;
 
        Assert.Null(table.Cells[0, 0]);
        Assert.Null(cell.Table);
    }
 
    [Fact]
    public void MergedCells_PreserveAnchorSpansRolesAndDistinctCoveredPositions()
    {
        ExtractionDocument document = new("merged");
        ExtractionDocumentSection section = new(null);
        ExtractionDocumentTableElement table = new(null, 3, 3);
        ExtractionDocumentTableCell anchor = new(new ExtractionDocumentTextElement(new TextContent("MERGED_A")), 2, 2, "header");
        ExtractionDocumentTableCell ordinary = new(new ExtractionDocumentTextElement(new TextContent("B")));
        table.Cells[0, 0] = anchor;
        table.Cells[2, 0] = ordinary;
        section.Elements.Add(table);
        document.Sections.Add(section);
 
        Assert.Equal(2, anchor.RowSpan);
        Assert.Equal(2, anchor.ColumnSpan);
        Assert.Equal("header", anchor.Role);
        Assert.False(table.Cells.IsCovered(0, 0));
        foreach ((int row, int column) in new[] { (0, 1), (1, 0), (1, 1) })
        {
            Assert.Null(table.Cells[row, column]);
            Assert.True(table.Cells.IsCovered(row, column));
            Assert.Same(anchor, table.Cells.GetCoveringCell(row, column));
        }
 
        Assert.Null(table.Cells[2, 2]);
        Assert.False(table.Cells.IsCovered(2, 2));
        Assert.Null(table.Cells.GetCoveringCell(2, 2));
        Assert.Equal(new ExtractionDocumentTableCell?[] { anchor, null, null, null, null, null, ordinary, null, null }, table.Cells.ToArray());
    }
 
    [Fact]
    public void MergedCells_OverlapCoveredEditsAndReuseRejectWithoutMutation()
    {
        ExtractionDocument document = new("invalid-merged");
        ExtractionDocumentSection section = new(null);
        ExtractionDocumentTableElement table = new(null, 3, 3);
        ExtractionDocumentTextElement text = new(new TextContent("MERGED_A"));
        using DocumentAnnotationTestData.CountingStream stream = new(new byte[] { 1, 2, 3 });
        text.Sources.Add(new(new("merged-page", 1), rawRepresentation: stream));
        ExtractionDocumentTableCell anchor = new(text, 2, 2);
        table.Cells[0, 0] = anchor;
        section.Elements.Add(table);
        document.Sections.Add(section);
        ExtractionDocumentTableCell?[] before = table.Cells.ToArray();
        ExtractionDocumentTableCell overlap = new(2, 1);
        ExtractionDocumentTableCell candidate = new();
 
        Assert.Throws<InvalidOperationException>(() => table.Cells[0, 1] = candidate);
        Assert.Throws<InvalidOperationException>(() => table.Cells[1, 1] = null);
        Assert.Throws<InvalidOperationException>(() => table.Cells[1, 0] = null);
        table.Cells[2, 2] = candidate;
        Assert.Throws<InvalidOperationException>(() => table.Cells[1, 2] = overlap);
        Assert.Throws<InvalidOperationException>(() => table.Cells[2, 1] = candidate);
        table.Cells[2, 2] = null;
        Assert.Equal(before, table.Cells.ToArray());
        Assert.Equal(new byte[] { 1, 2, 3 }, stream.ToArray());
        Assert.Equal(0, stream.DisposeCount);
 
        table.Cells[0, 0] = null;
        Assert.False(table.Cells.IsCovered(1, 1));
        Assert.Null(table.Cells.GetCoveringCell(1, 1));
        Assert.Equal(0, stream.DisposeCount);
    }
 
    [Theory]
    [InlineData(2, 1)]
    [InlineData(1, 2)]
    public void MergedCells_OutOfBoundsSpanRejectsWithoutTakingOwnership(int rowSpan, int columnSpan)
    {
        ExtractionDocument document = new("bounds");
        ExtractionDocumentSection section = new(null);
        ExtractionDocumentTableElement table = new(null, 3, 3);
        section.Elements.Add(table);
        document.Sections.Add(section);
        ExtractionDocumentTableCell candidate = new(rowSpan, columnSpan);
 
        Assert.Throws<ArgumentException>("cell", () => table.Cells[2, 2] = candidate);
        Assert.Null(table.Cells[2, 2]);
        table.Cells[0, 0] = candidate;
    }
 
    [Fact]
    public void MergedCells_ProspectiveSpanCrossingCoveredSlotOnlyRejectsWithoutMutation()
    {
        ExtractionDocument document = new("covered-slot-overlap");
        ExtractionDocumentSection section = new(null);
        ExtractionDocumentTableElement table = new(null, 3, 3);
        ExtractionDocumentTextElement anchorText = new(new TextContent("MERGED_A"));
        ExtractionDocumentTableCell anchor = new(anchorText, 2, 2);
        table.Cells[1, 1] = anchor;
        section.Elements.Add(table);
        document.Sections.Add(section);
        using DocumentAnnotationTestData.CountingStream stream = new(new byte[] { 61, 62, 63 });
        ExtractionDocumentTextElement candidateText = new(new TextContent("candidate **value**"));
        candidateText.Sources.Add(new(new("candidate-page", 1), rawRepresentation: stream));
        ExtractionDocumentTableCell candidate = new(candidateText, 2, 1);
        ExtractionDocumentTableCell?[] before = table.Cells.ToArray();
 
        // Candidate occupies (0,2) and (1,2), never the existing anchor at (1,1).
        Assert.Null(table.Cells[0, 2]);
        Assert.False(table.Cells.IsCovered(0, 2));
        Assert.Null(table.Cells.GetCoveringCell(0, 2));
        Assert.Null(table.Cells[1, 2]);
        Assert.True(table.Cells.IsCovered(1, 2));
        Assert.Same(anchor, table.Cells.GetCoveringCell(1, 2));
 
        Assert.Throws<InvalidOperationException>(() => table.Cells[0, 2] = candidate);
 
        Assert.Equal(before, table.Cells.ToArray());
        for (int row = 0; row < 3; row++)
        {
            for (int column = 0; column < 3; column++)
            {
                bool inAnchor = row >= 1 && column >= 1;
                Assert.Equal(inAnchor && (row != 1 || column != 1), table.Cells.IsCovered(row, column));
                Assert.Same(inAnchor ? anchor : null, table.Cells.GetCoveringCell(row, column));
            }
        }
 
        Assert.Same(anchor, table.Cells[1, 1]);
        Assert.Empty(document.SourcePages);
        Assert.Equal(new byte[] { 61, 62, 63 }, stream.ToArray());
        Assert.Equal(0, stream.DisposeCount);
 
        // Success in another attached table proves rejection took neither cell nor content ownership.
        ExtractionDocument other = new("candidate-owner");
        ExtractionDocumentSection otherSection = new(null);
        ExtractionDocumentTableElement otherTable = new(null, 2, 1);
        otherSection.Elements.Add(otherTable);
        other.Sections.Add(otherSection);
        otherTable.Cells[0, 0] = candidate;
        other.SourcePages.Add(candidateText.Sources[0].Page);
        Assert.Same(candidate, otherTable.Cells[0, 0]);
        Assert.Same(candidate, otherTable.Cells.GetCoveringCell(1, 0));
        Assert.True(otherTable.Cells.IsCovered(1, 0));
        Assert.Same(candidateText.Sources[0].Page, Assert.Single(other.SourcePages));
        Assert.Equal("candidate **value**", Assert.IsType<TextContent>(candidateText.Content).Text);
        Assert.Equal(before, table.Cells.ToArray());
 
        Assert.Equal(0, stream.DisposeCount);
        Assert.Equal(new byte[] { 61, 62, 63 }, stream.ToArray());
        Assert.Equal(0, stream.DisposeCount);
        stream.Dispose();
        Assert.False(stream.CanRead);
    }
 
    [Fact]
    public void MergedAnchorReplacement_ReleasesCoveredSlotsAndRetainsExplicitSourceData()
    {
        ExtractionDocument document = new("replacement-lifetime");
        using DocumentAnnotationTestData.CountingStream stream = new(DocumentAnnotationTestData.OriginalBytes());
        using DocumentAnnotationTestData.BorrowedNative native = new();
        document.RawRepresentation = native;
        ExtractionDocumentSourcePage frame = DocumentAnnotationTestData.PdfPage();
        document.SourcePages.Add(frame);
        ExtractionDocumentTextElement oldText = new(new TextContent("lO0? &"));
        ExtractionDocumentTableCell oldAnchor = new(oldText, 2, 2, "rowHeader");
        oldAnchor.Sources.Add(new ExtractionDocumentSourceReference(frame, DocumentAnnotationTestData.Polygon()));
        ExtractionDocumentTableElement table = new(null, 4, 4);
        table.Cells[0, 0] = oldAnchor;
        ExtractionDocumentSection section = new(null);
        section.Elements.Add(table);
        document.Sections.Add(section);
        Assert.Same(oldAnchor, table.Cells.GetCoveringCell(1, 1));
        Assert.True(table.Cells.IsCovered(1, 1));
 
        ExtractionDocumentTextElement replacementText = new(new TextContent("replacement?"));
        ExtractionDocumentTableCell replacement = new(replacementText);
        table.Cells[0, 0] = replacement;
 
        Assert.Same(replacement, table.Cells.GetCoveringCell(0, 0));
        foreach ((int row, int column) in new[] { (0, 1), (1, 0), (1, 1) })
        {
            Assert.Null(table.Cells[row, column]);
            Assert.Null(table.Cells.GetCoveringCell(row, column));
            Assert.False(table.Cells.IsCovered(row, column));
        }
 
        Assert.Same(frame, Assert.Single(document.SourcePages));
        Assert.Equal(0, stream.DisposeCount);
        Assert.True(stream.CanRead);
        Assert.Equal(DocumentAnnotationTestData.OriginalBytes(), stream.ToArray());
 
        ExtractionDocumentTableCell empty = new();
        table.Cells[0, 1] = empty;
        Assert.Same(empty, table.Cells.GetCoveringCell(0, 1));
        Assert.False(table.Cells.IsCovered(0, 1));
        table.Cells[2, 2] = oldAnchor;
 
        Assert.Equal(4, table.Cells.RowCount);
        Assert.Equal(4, table.Cells.ColumnCount);
        Assert.Equal(2, oldAnchor.RowIndex);
        Assert.Equal(2, oldAnchor.ColumnIndex);
        Assert.Equal(2, oldAnchor.RowSpan);
        Assert.Equal(2, oldAnchor.ColumnSpan);
        Assert.Equal("rowHeader", oldAnchor.Role);
        Assert.Equal("lO0? &", Assert.IsType<TextContent>(oldText.Content).Text);
        Assert.Equal("replacement?", Assert.IsType<TextContent>(replacementText.Content).Text);
        Assert.Null(empty.Content);
        for (int row = 0; row < 4; row++)
        {
            for (int column = 0; column < 4; column++)
            {
                ExtractionDocumentTableCell? covering = null;
                if (row >= 2 && column >= 2)
                {
                    covering = oldAnchor;
                }
                else if (row == 0 && column == 0)
                {
                    covering = replacement;
                }
                else if (row == 0 && column == 1)
                {
                    covering = empty;
                }
 
                Assert.Same(covering, table.Cells.GetCoveringCell(row, column));
                Assert.Equal(row >= 2 && column >= 2 && (row != 2 || column != 2), table.Cells.IsCovered(row, column));
            }
        }
 
        Assert.Equal(
            new ExtractionDocumentTableCell?[]
            {
                replacement, empty, null, null, null, null, null, null,
                null, null, oldAnchor, null, null, null, null, null
            },
            table.Cells.ToArray());
        Assert.Null(table.Content);
        Assert.Same(frame, Assert.Single(document.SourcePages));
        Assert.Same(frame, oldAnchor.Sources[0].Page);
        Assert.Equal(DocumentAnnotationTestData.Polygon(), oldAnchor.Sources[0].Polygon);
        Assert.Same(native, document.RawRepresentation);
        Assert.Equal(0, stream.DisposeCount);
        Assert.Equal(0, native.DisposeCount);
        Assert.True(stream.CanRead);
        Assert.Equal(DocumentAnnotationTestData.OriginalBytes(), stream.ToArray());
 
        stream.Dispose();
        Assert.Equal(1, stream.DisposeCount);
        Assert.False(stream.CanRead);
        Assert.Equal(0, native.DisposeCount);
    }
}