Files
ClosedXML-ClosedXML/ClosedXML.Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs
T
dualfroz 08d598d941 fix: write data bar cfvo as min/max instead of num for lowest/highest value
DataBar().LowestValue()/HighestValue() emitted the x14 extension cfvo nodes as type="num" with a formula of 0 instead of type="min"/"max". Strict readers such as WPS Spreadsheets honor that value and lock the bar thresholds to 0, so the data bar renders incorrectly.

The extension converter unconditionally overwrote the cfvo type with Numeric whenever a threshold value was present. LowestValue()/HighestValue() store a placeholder "0", so their automatic min/max bounds were turned into fixed numeric thresholds; the same override also collapsed percent, percentile and formula thresholds to num.

Each cfvo is now built from its content type: automatic bounds stay autoMin/autoMax, lowest/highest bounds become min/max with no formula, and value based thresholds keep their type and emit the formula.

Fixes #2843
2026-09-02 11:15:38 +02:00

277 lines
13 KiB
C#

using System;
using ClosedXML.Excel;
using NUnit.Framework;
using System.Globalization;
using System.IO;
using System.IO.Packaging;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using SaveOptions = ClosedXML.Excel.SaveOptions;
namespace ClosedXML.Tests.Excel.ConditionalFormats
{
[TestFixture]
public class ConditionalFormatTests
{
[Test]
public void DataBarLowestHighestValueWritesMinMaxCfvoWithoutFormula()
{
// https://github.com/ClosedXML/ClosedXML/issues/2843
// DataBar().LowestValue()/HighestValue() must emit the x14 cfvo nodes as
// type="min"/type="max" without an xm:f formula. The regression wrote them
// as type="num" locked to 0, which strict parsers (e.g. WPS) render wrong.
XNamespace x14 = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main";
using var ms = new MemoryStream();
using (var wb = new XLWorkbook())
{
var ws = wb.AddWorksheet("Sheet1");
ws.Cell("A1").Value = 1;
ws.Cell("A2").Value = -1;
ws.Cell("A3").Value = 2;
ws.Range("A1:A3").AddConditionalFormat()
.DataBar(XLColor.Red)
.LowestValue()
.HighestValue();
wb.SaveAs(ms);
}
ms.Position = 0;
using var package = Package.Open(ms, FileMode.Open, FileAccess.Read);
var part = package.GetPart(new Uri("/xl/worksheets/sheet1.xml", UriKind.Relative));
using var partStream = part.GetStream();
var doc = XDocument.Load(partStream);
var x14DataBar = doc.Descendants(x14 + "dataBar").Single();
var cfvos = x14DataBar.Elements(x14 + "cfvo").ToList();
Assert.AreEqual(2, cfvos.Count);
Assert.AreEqual("min", cfvos[0].Attribute("type")?.Value);
Assert.AreEqual("max", cfvos[1].Attribute("type")?.Value);
Assert.IsFalse(
x14DataBar.Descendants().Any(e => e.Name.LocalName == "f"),
"x14 cfvo for lowest/highest value must not contain a formula.");
}
[Test]
public void MaintainConditionalFormattingOrder()
{
// The input file contains duplicates of same dxf and thus the output also contains some duplicate dxf.
TestHelper.LoadSaveAndCompare(
@"Other\StyleReferenceFiles\ConditionalFormattingOrder\inputfile.xlsx",
@"Other\StyleReferenceFiles\ConditionalFormattingOrder\ConditionalFormattingOrder.xlsx");
}
[TestCase(true, 7)]
[TestCase(false, 8)]
public void SaveOptionAffectsConsolidationConditionalFormatRanges(bool consolidateConditionalFormatRanges, int expectedCount)
{
var options = new SaveOptions
{
ConsolidateConditionalFormatRanges = consolidateConditionalFormatRanges
};
var wb = new XLWorkbook();
var ws = wb.AddWorksheet("Sheet");
ws.Range("D2:D3").AddConditionalFormat().DataBar(XLColor.Red).LowestValue().HighestValue();
ws.Range("B2:B3").AddConditionalFormat().DataBar(XLColor.Red).LowestValue().HighestValue();
ws.Range("E2:E6").AddConditionalFormat().ColorScale().LowestValue(XLColor.Red).HighestValue(XLColor.Blue);
ws.Range("F2:F6").AddConditionalFormat().ColorScale().LowestValue(XLColor.Red).HighestValue(XLColor.Blue);
ws.Range("G2:G7").AddConditionalFormat().WhenIsUnique().Fill.SetBackgroundColor(XLColor.Blue);
ws.Range("H2:H7").AddConditionalFormat().WhenIsUnique().Fill.SetBackgroundColor(XLColor.Blue);
ws.Range("I2:I6").AddConditionalFormat().WhenContains("test");
ws.Range("J2:J6").AddConditionalFormat().WhenContains("test");
using (var ms = new MemoryStream())
{
wb.SaveAs(ms, options);
var wb_saved = new XLWorkbook(ms);
Assert.AreEqual(expectedCount, wb_saved.Worksheet("Sheet").ConditionalFormats.Count());
}
}
[TestCase(true, 1)]
[TestCase(false, 2)]
public void SaveOptionAffectsConsolidationDataValidationRanges(bool consolidateDataValidationRanges, int expectedCount)
{
var options = new SaveOptions
{
ConsolidateDataValidationRanges = consolidateDataValidationRanges
};
var wb = new XLWorkbook();
var ws = wb.AddWorksheet("Sheet");
ws.Range("C2:C5").CreateDataValidation().Decimal.Between(1, 5);
ws.Range("D2:D5").CreateDataValidation().Decimal.Between(1, 5);
using (var ms = new MemoryStream())
{
wb.SaveAs(ms, options);
var wb_saved = new XLWorkbook(ms);
Assert.AreEqual(expectedCount, wb_saved.Worksheet("Sheet").DataValidations.Count());
}
}
[TestCase("en-US")]
[TestCase("fr-FR")]
[TestCase("ru-RU")]
public void SaveConditionalFormat_CultureIndependent(string culture)
{
using (var ms = new MemoryStream())
{
var expectedValue = 1.5;
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture);
using (var wb = new XLWorkbook())
{
var ws = wb.AddWorksheet();
var i = 1;
ws.Cell(i++, 1).AddConditionalFormat().WhenEquals(expectedValue).Fill.SetBackgroundColor(XLColor.Red);
ws.Cell(i++, 1).AddConditionalFormat().WhenNotEquals(expectedValue).Fill.SetBackgroundColor(XLColor.Red);
ws.Cell(i++, 1).AddConditionalFormat().WhenGreaterThan(expectedValue).Fill.SetBackgroundColor(XLColor.Red);
ws.Cell(i++, 1).AddConditionalFormat().WhenLessThan(expectedValue).Fill.SetBackgroundColor(XLColor.Red);
ws.Cell(i++, 1).AddConditionalFormat().WhenEqualOrGreaterThan(expectedValue).Fill.SetBackgroundColor(XLColor.Red);
ws.Cell(i++, 1).AddConditionalFormat().WhenEqualOrLessThan(expectedValue).Fill.SetBackgroundColor(XLColor.Red);
ws.Cell(i++, 1).AddConditionalFormat().WhenBetween(expectedValue, expectedValue).Fill.SetBackgroundColor(XLColor.Red);
ws.Cell(i++, 1).AddConditionalFormat().WhenNotBetween(expectedValue, expectedValue).Fill.SetBackgroundColor(XLColor.Red);
wb.SaveAs(ms);
}
using (var wb = new XLWorkbook(ms))
{
var ws = wb.Worksheets.First();
var conditionalFormatValues = ws.ConditionalFormats
.SelectMany(cf => cf.Values.Values)
.Select(v => v.Value)
.Distinct();
Assert.AreEqual(1, conditionalFormatValues.Count());
Assert.AreEqual("1.5", conditionalFormatValues.Single());
}
}
}
[Test]
public void CellIs_type_reads_only_required_formula_arguments()
{
// The CellIs uses formula tags as arguments. Some producers generate extra empty
// formula tags and ClosedXml should be able to load CellIs conditional formatting
// with such extra tags without an exception. The test file has been modified to
// include extra formula tags and test checks that extra tags are ignored.
TestHelper.LoadAndAssert((_, ws) =>
{
AssertFormulaArgs(ws, XLCFOperator.Between, "$D$2", "$E$2");
AssertFormulaArgs(ws, XLCFOperator.NotBetween, "$D$3", "$E$3");
AssertFormulaArgs(ws, XLCFOperator.GreaterThan, "$D$4");
AssertFormulaArgs(ws, XLCFOperator.LessThan, "$D$5");
AssertFormulaArgs(ws, XLCFOperator.Equal, "$D$6");
}, @"Other\ConditionalFormats\Extra_formulas_CellIs_type.xlsx");
static void AssertFormulaArgs(IXLWorksheet ws, XLCFOperator cfOperator, params string[] expectedFormulas)
{
var cf = ws.ConditionalFormats.Single(cf => cf.ConditionalFormatType == XLConditionalFormatType.CellIs && cf.Operator == cfOperator);
Assert.AreEqual(expectedFormulas.Length, cf.Values.Count);
CollectionAssert.AreEqual(expectedFormulas, cf.Values.Select(v => v.Value.Value));
}
}
[Test]
public void Expression_type_skips_empty_formula_tags()
{
// The Expression uses formula tag as arguments. Some producers generate extra empty
// formula tags and ClosedXml should be able to load Expression conditional formatting
// with such extra tags without an exception. The test file has been modified to
// include extra formula tags and test checks that extra tags are ignored.
TestHelper.LoadAndAssert((_, ws) =>
{
AssertFormulaArgs(ws, "A1:A1", "$C$1=5");
AssertFormulaArgs(ws, "A2:A2", "$C$2=4");
}, @"Other\ConditionalFormats\Extra_formulas_Expression_type.xlsx");
static void AssertFormulaArgs(IXLWorksheet ws, string range, string expectedFormula)
{
var cf = ws.ConditionalFormats.Single(cf => cf.ConditionalFormatType == XLConditionalFormatType.Expression && cf.Range.RangeAddress.ToString() == range);
Assert.AreEqual(1, cf.Values.Count);
CollectionAssert.AreEqual(expectedFormula, cf.Values[1].Value);
}
}
[Test]
public void Range_setter_throws_on_range_from_different_worksheet()
{
using var wb = new XLWorkbook();
var ws1 = wb.AddWorksheet();
var cf = ws1.Range("A1").AddConditionalFormat();
var ws2 = wb.AddWorksheet();
var differentSheetRange = ws2.Range("B5");
Assert.Throws<ArgumentException>(() => cf.Range = differentSheetRange);
}
[Test]
public void Ranges_setter_throws_on_range_from_different_worksheet()
{
using var wb = new XLWorkbook();
var ws1 = wb.AddWorksheet();
var cf = ws1.Range("A1").AddConditionalFormat();
var ranges = ws1.Ranges("B1");
var differentSheetRange = wb.AddWorksheet().Range("C1");
ranges.Add(differentSheetRange);
Assert.That(() => cf.Ranges = ranges, Throws.TypeOf<ArgumentException>().With.Message.Contains("must be from worksheet"));
}
[Test]
public void Ranges_setter_throws_on_empty_ranges()
{
using var wb = new XLWorkbook();
var ws1 = wb.AddWorksheet();
var cf = ws1.Range("A1").AddConditionalFormat();
var emptyRanges = ws1.Ranges("");
Assert.That(() => cf.Ranges = emptyRanges, Throws.TypeOf<ArgumentException>().With.Message.Contains("empty"));
}
[Test]
public void Text_formats_are_loaded_as_value()
{
// Issue #2690: Ensure that CF for text are loaded as a value and not as a formula.
TestHelper.CreateSaveLoadAssert(
(_, ws) =>
{
ws.AddConditionalFormat().WhenContains("Contains")
.Fill.SetBackgroundColor(XLColor.Red);
ws.AddConditionalFormat().WhenNotContains("Not Contains")
.Fill.SetBackgroundColor(XLColor.Green);
ws.AddConditionalFormat().WhenStartsWith("Starts With")
.Fill.SetBackgroundColor(XLColor.Blue);
ws.AddConditionalFormat().WhenEndsWith("Ends With")
.Fill.SetBackgroundColor(XLColor.Black);
},
(_, ws) =>
{
AssertTextCfValue(ws, XLConditionalFormatType.ContainsText, "Contains");
AssertTextCfValue(ws, XLConditionalFormatType.NotContainsText, "Not Contains");
AssertTextCfValue(ws, XLConditionalFormatType.StartsWith, "Starts With");
AssertTextCfValue(ws, XLConditionalFormatType.EndsWith, "Ends With");
});
return;
static void AssertTextCfValue(IXLWorksheet ws, XLConditionalFormatType type, string text)
{
var cf = ws.ConditionalFormats.Single(x => x.ConditionalFormatType == type);
Assert.AreEqual(1, cf.Values.Count);
Assert.AreEqual(text, cf.Values[1].Value);
Assert.IsFalse(cf.Values[1].IsFormula);
}
}
}
}