using ClosedXML.Excel; using Xunit; namespace WorkTracker.Tests; internal static class WorkbookAssert { public static void Equivalent(string expectedPath, byte[] actualContent) { using var expectedWorkbook = new XLWorkbook(expectedPath); using var actualStream = new MemoryStream(actualContent); using var actualWorkbook = new XLWorkbook(actualStream); var expectedWorksheet = expectedWorkbook.Worksheet(1); var actualWorksheet = actualWorkbook.Worksheet(1); Assert.Equal(expectedWorksheet.Name, actualWorksheet.Name); Assert.Equal(expectedWorksheet.RangeUsed()?.RangeAddress.ToString(), actualWorksheet.RangeUsed()?.RangeAddress.ToString()); Assert.Equal(expectedWorksheet.MergedRanges.Select(range => range.RangeAddress.ToString()), actualWorksheet.MergedRanges.Select(range => range.RangeAddress.ToString())); var usedRange = expectedWorksheet.RangeUsed() ?? throw new InvalidOperationException("Expected workbook must have a used range."); foreach (var column in Enumerable.Range(usedRange.RangeAddress.FirstAddress.ColumnNumber, usedRange.ColumnCount())) { Assert.Equal(Math.Round(expectedWorksheet.Column(column).Width, 5), Math.Round(actualWorksheet.Column(column).Width, 5)); } foreach (var cell in usedRange.Cells()) { var actualCell = actualWorksheet.Cell(cell.Address.RowNumber, cell.Address.ColumnNumber); Assert.Equal(GetCellValue(cell), GetCellValue(actualCell)); Assert.Equal(cell.FormulaA1, actualCell.FormulaA1); if (string.IsNullOrEmpty(cell.FormulaA1)) { Assert.Equal(cell.DataType, actualCell.DataType); } var expectedStyle = DescribeStyle(cell.Style); var actualStyle = DescribeStyle(actualCell.Style); Assert.True(expectedStyle == actualStyle, $"Style mismatch at {cell.Address}: expected '{expectedStyle}' actual '{actualStyle}'"); } } private static string GetCellValue(IXLCell cell) { if (!string.IsNullOrEmpty(cell.FormulaA1)) { return string.Empty; } return cell.Value.ToString(); } private static string DescribeStyle(IXLStyle style) { return string.Join( "|", style.NumberFormat.NumberFormatId, style.NumberFormat.Format, style.Fill.PatternType, DescribeColor(style.Fill.BackgroundColor), DescribeColor(style.Fill.PatternColor), style.Font.FontName, style.Font.FontSize, style.Font.Bold, style.Alignment.Horizontal, style.Alignment.Vertical, style.Alignment.WrapText, style.Border.LeftBorder, DescribeColor(style.Border.LeftBorderColor), style.Border.RightBorder, DescribeColor(style.Border.RightBorderColor), style.Border.TopBorder, DescribeColor(style.Border.TopBorderColor), style.Border.BottomBorder, DescribeColor(style.Border.BottomBorderColor), style.Protection.Locked); } private static string DescribeColor(XLColor color) { return color.ColorType switch { XLColorType.Color => $"rgb:{color.Color.ToArgb()}", XLColorType.Indexed => $"indexed:{color.Indexed}", XLColorType.Theme => $"theme:{color.ThemeColor}:{Math.Round(color.ThemeTint, 12)}", _ => color.ColorType.ToString() }; } }