mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-07-08 18:16:08 +00:00
The spreadsheet engine parsed typed input with the invariant culture while displaying values with the current culture, breaking the edit round-trip on non-en-US hosts and making comma-decimal entry (10,50) impossible. Workbook gains a Culture property (defaults to CurrentCulture) which the component stamps from its inherited Culture parameter. It drives: - Cell input parsing (CellData type inference), including day-month date handling for cultures where the group and date separators collide (de-DE) - Edit text and display rendering (GetValue, GetValueAsString, GetDisplayText) - Number format rendering - format codes stay canonical invariant tokens while separators, month names, and AM/PM designators follow the culture - Formula entry and display via the new FormulaLocalizer (Excel FormulaLocal semantics: ';' argument separators and ',' decimals in comma-decimal cultures, lenient comma acceptance where unambiguous, canonical invariant storage) - Dialog input (data validation, conditional format, filter) via shared conversion helpers on SpreadsheetDialogBase XLSX and CSV files read and write canonical invariant values regardless of the workbook culture, including autofit column widths. Malformed formulas now surface as error trees instead of an unhandled lexer exception. Number format parsing is cached (hard-bounded) since CellView reparsed per render. Includes localization demos for the Spreadsheet and Document Processing sections and culture test suites. Note: headless code on non-en-US hosts now parses string values with the host culture; set Workbook.Culture explicitly (or to InvariantCulture) for host-independent processing.
1266 lines
43 KiB
C#
1266 lines
43 KiB
C#
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Bunit;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.JSInterop;
|
|
using Radzen.Blazor;
|
|
using Radzen.Blazor.Spreadsheet;
|
|
using Radzen.Documents.Spreadsheet;
|
|
using Xunit;
|
|
|
|
namespace Radzen.Blazor.Tests;
|
|
|
|
public class SpreadsheetTests
|
|
{
|
|
private static TestContext CreateContext()
|
|
{
|
|
var ctx = new TestContext();
|
|
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
|
|
// VirtualGrid relies on a JS-measured region. Loose mode returns null for class types,
|
|
// which would NRE inside its first render. Hand it a stub so OnAfterRender completes.
|
|
ctx.JSInterop.Setup<VirtualRegion>("Radzen.createVirtualItemContainer", _ => true)
|
|
.SetResult(new VirtualRegion { Width = 800, Height = 600, ScrollWidth = 800, ScrollHeight = 600 });
|
|
ctx.Services.AddRadzenComponents();
|
|
return ctx;
|
|
}
|
|
|
|
private static Workbook NewWorkbook()
|
|
{
|
|
var wb = new Workbook();
|
|
wb.AddSheet("Sheet1", 10, 10);
|
|
return wb;
|
|
}
|
|
|
|
[Fact]
|
|
public void CultureParameter_StampsWorkbookAndRerendersFormattedCells()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = 1234.5;
|
|
sheet.Cells[0, 0].Format.NumberFormat = "#,##0.00";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p
|
|
.Add(x => x.Workbook, wb)
|
|
.Add(x => x.Culture, CultureInfo.InvariantCulture));
|
|
|
|
Assert.Same(CultureInfo.InvariantCulture, wb.Culture);
|
|
Assert.Contains("1,234.50", c.Markup);
|
|
|
|
var german = CultureInfo.GetCultureInfo("de-DE");
|
|
c.SetParametersAndRender(p => p.Add(x => x.Culture, german));
|
|
|
|
Assert.Same(german, wb.Culture);
|
|
Assert.Contains("1.234,50", c.Markup);
|
|
}
|
|
|
|
[Fact]
|
|
public void HandSetWorkbookCulture_SurvivesParameterSets()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p
|
|
.Add(x => x.Workbook, wb)
|
|
.Add(x => x.Culture, CultureInfo.InvariantCulture));
|
|
|
|
var german = CultureInfo.GetCultureInfo("de-DE");
|
|
wb.Culture = german;
|
|
|
|
c.SetParametersAndRender(p => p.Add(x => x.ReadOnly, true));
|
|
|
|
Assert.Same(german, wb.Culture);
|
|
}
|
|
|
|
// ExecuteAsync triggers cell-change StateHasChanged callbacks that have to run on
|
|
// the Blazor dispatcher. Wrap with InvokeAsync so tests can call the API freely.
|
|
private static async Task<bool> Run(IRenderedComponent<RadzenSpreadsheet> c, ICommand command)
|
|
{
|
|
var result = false;
|
|
await c.InvokeAsync(async () => result = await c.Instance.ExecuteAsync(command));
|
|
return result;
|
|
}
|
|
|
|
// ── Default rendering ───────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Spreadsheet_Renders_HostCssClass()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>();
|
|
Assert.Contains("rz-spreadsheet", c.Markup);
|
|
}
|
|
|
|
[Fact]
|
|
public void Spreadsheet_Defaults_RenderToolbarFormulaBarAndSheetTabs()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, NewWorkbook()));
|
|
|
|
Assert.Contains("rz-tabview", c.Markup); // toolbar
|
|
Assert.Contains("rz-spreadsheet-formula-editor", c.Markup); // formula bar
|
|
Assert.Contains("rz-spreadsheet-sheet-tabs", c.Markup); // sheet strip
|
|
}
|
|
|
|
// ── ShowToolbar / ShowFormulaBar / ShowSheetTabs ────────────────────
|
|
|
|
[Fact]
|
|
public void ShowToolbar_False_HidesToolbarToolsetLabels()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, NewWorkbook());
|
|
p.Add(x => x.ShowToolbar, false);
|
|
});
|
|
|
|
// Every toolset label is gone; the bottom sheet-tabs strip stays.
|
|
var home = RadzenStrings.ResourceManager.GetString(nameof(RadzenStrings.Spreadsheet_HomeTab))!;
|
|
var insert = RadzenStrings.ResourceManager.GetString(nameof(RadzenStrings.Spreadsheet_InsertTab))!;
|
|
Assert.DoesNotContain(home, c.Markup);
|
|
Assert.DoesNotContain(insert, c.Markup);
|
|
}
|
|
|
|
[Fact]
|
|
public void ShowFormulaBar_False_HidesFormulaEditor()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, NewWorkbook());
|
|
p.Add(x => x.ShowFormulaBar, false);
|
|
});
|
|
|
|
Assert.DoesNotContain("rz-spreadsheet-formula-editor", c.Markup);
|
|
}
|
|
|
|
[Fact]
|
|
public void ShowSheetTabs_False_HidesSheetTabsStrip()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, NewWorkbook());
|
|
p.Add(x => x.ShowSheetTabs, false);
|
|
});
|
|
|
|
Assert.DoesNotContain("rz-spreadsheet-sheet-tabs", c.Markup);
|
|
}
|
|
|
|
// ── ChildContent (custom toolbar) ──
|
|
|
|
[Fact]
|
|
public void ChildContent_Replaces_DefaultToolsets()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, NewWorkbook());
|
|
p.AddChildContent("<RadzenTabsItem Text=\"My Tab\"><div>my tools</div></RadzenTabsItem>");
|
|
});
|
|
|
|
// Default labels are gone, the supplied toolset is rendered, the toolbar shell stays.
|
|
var home = RadzenStrings.ResourceManager.GetString(nameof(RadzenStrings.Spreadsheet_HomeTab))!;
|
|
Assert.DoesNotContain(home, c.Markup);
|
|
Assert.Contains("My Tab", c.Markup);
|
|
Assert.Contains("rz-tabview", c.Markup);
|
|
}
|
|
|
|
[Fact]
|
|
public void ChildContent_DoesNotInclude_TableDesignToolset_AutomaticallyInsideTable()
|
|
{
|
|
// ChildContent fully replaces the toolbar — the host does NOT inject the
|
|
// Table Design tab. Users add the TableDesignToolset component themselves to opt in.
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.AddTable("T1", RangeRef.Parse("A1:B2"));
|
|
sheet.Selection.Select(CellRef.Parse("A1"));
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.AddChildContent("<RadzenTabsItem Text=\"Only Mine\"><div /></RadzenTabsItem>");
|
|
});
|
|
|
|
var tableDesign = RadzenStrings.ResourceManager.GetString(nameof(RadzenStrings.Spreadsheet_TableDesignTab))!;
|
|
Assert.Contains("Only Mine", c.Markup);
|
|
Assert.DoesNotContain(tableDesign, c.Markup);
|
|
}
|
|
|
|
[Fact]
|
|
public void TableDesignToolset_InChildContent_ShowsWhenCellInsideTable()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.AddTable("T1", RangeRef.Parse("A1:B2"));
|
|
sheet.Selection.Select(CellRef.Parse("A1"));
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.AddChildContent<Radzen.Blazor.RadzenSpreadsheetTableDesignToolset>(tab =>
|
|
tab.Add(t => t.Worksheet, sheet));
|
|
});
|
|
|
|
var tableDesign = RadzenStrings.ResourceManager.GetString(nameof(RadzenStrings.Spreadsheet_TableDesignTab))!;
|
|
Assert.Contains(tableDesign, c.Markup);
|
|
}
|
|
|
|
[Fact]
|
|
public void TableDesignToolset_InChildContent_HiddenWhenCellOutsideTable()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.AddTable("T1", RangeRef.Parse("A1:B2"));
|
|
sheet.Selection.Select(CellRef.Parse("E5")); // outside table
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.AddChildContent<Radzen.Blazor.RadzenSpreadsheetTableDesignToolset>(tab =>
|
|
tab.Add(t => t.Worksheet, sheet));
|
|
});
|
|
|
|
var tableDesign = RadzenStrings.ResourceManager.GetString(nameof(RadzenStrings.Spreadsheet_TableDesignTab))!;
|
|
Assert.DoesNotContain(tableDesign, c.Markup);
|
|
}
|
|
|
|
// ── ReadOnly ────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task ReadOnly_True_RejectsExecuteAsync()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.ReadOnly, true);
|
|
});
|
|
|
|
var ran = await Run(c,new ClearContentsCommand(sheet, RangeRef.Parse("A1")));
|
|
|
|
Assert.False(ran);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadOnly_False_AllowsExecuteAsync()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells["A1"].Value = "hello";
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
var ran = await Run(c,new ClearContentsCommand(sheet, RangeRef.Parse("A1")));
|
|
|
|
Assert.True(ran);
|
|
Assert.Null(sheet.Cells["A1"].Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReadOnly_True_DisablesAddSheetButton()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, NewWorkbook());
|
|
p.Add(x => x.ReadOnly, true);
|
|
});
|
|
|
|
// The add-sheet button is inside the sheet tabs strip and should be disabled.
|
|
var addButton = c.Find(".rz-spreadsheet-add-sheet");
|
|
Assert.True(addButton.HasAttribute("disabled"));
|
|
}
|
|
|
|
// ── Allow* gating ───────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task AllowEditing_False_RejectsClearContents()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.AllowEditing, false);
|
|
});
|
|
|
|
var ran = await Run(c,new ClearContentsCommand(sheet, RangeRef.Parse("A1")));
|
|
|
|
Assert.False(ran);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AllowFiltering_False_RejectsFilterCommand()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.AllowFiltering, false);
|
|
});
|
|
|
|
var filter = new SheetFilter(new EqualToCriterion { Column = 0, Value = "x" }, RangeRef.Parse("A1:A5"));
|
|
var ran = await Run(c,new FilterCommand(sheet, filter));
|
|
|
|
Assert.False(ran);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AllowSorting_False_RejectsSortCommand()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.AllowSorting, false);
|
|
});
|
|
|
|
var ran = await Run(c,new SortCommand(sheet, RangeRef.Parse("A1:A5"), SortOrder.Ascending, 0));
|
|
|
|
Assert.False(ran);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AllowCellFormatting_False_RejectsFormatCommand()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.AllowCellFormatting, false);
|
|
});
|
|
|
|
var ran = await Run(c,new FormatCommand(sheet, RangeRef.Parse("A1"), new Format { Bold = true }));
|
|
|
|
Assert.False(ran);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AllowMerge_False_RejectsMergeCommand()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.AllowMerge, false);
|
|
});
|
|
|
|
var ran = await Run(c,new MergeCellsCommand(sheet, RangeRef.Parse("A1:B2")));
|
|
|
|
Assert.False(ran);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AllowImages_False_RejectsInsertImage()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.AllowImages, false);
|
|
});
|
|
|
|
var image = new SheetImage { From = new CellAnchor { Row = 0, Column = 0 }, Width = 100, Height = 100 };
|
|
var ran = await Run(c,new InsertImageCommand(sheet, image));
|
|
|
|
Assert.False(ran);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AllowCharts_False_RejectsInsertChart()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.AllowCharts, false);
|
|
});
|
|
|
|
var chart = new SheetChart { From = new CellAnchor { Row = 0, Column = 0 } };
|
|
var ran = await Run(c,new InsertChartCommand(sheet, chart));
|
|
|
|
Assert.False(ran);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AllowTables_False_RejectsInsertTable()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.AllowTables, false);
|
|
});
|
|
|
|
var ran = await Run(c,new InsertTableCommand(sheet, "T1", RangeRef.Parse("A1:B2")));
|
|
|
|
Assert.False(ran);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AllowHyperlinks_False_RejectsHyperlinkCommand()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.AllowHyperlinks, false);
|
|
});
|
|
|
|
var ran = await Run(c,new HyperlinkCommand(sheet, CellRef.Parse("A1"), new Hyperlink { Url = "https://example.com" }));
|
|
|
|
Assert.False(ran);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AllowDataValidation_False_RejectsDataValidation()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.AllowDataValidation, false);
|
|
});
|
|
|
|
var rule = new DataValidationRule { Type = DataValidationType.WholeNumber };
|
|
var ran = await Run(c,new DataValidationCommand(sheet, RangeRef.Parse("A1"), rule));
|
|
|
|
Assert.False(ran);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AllowConditionalFormatting_False_RejectsConditionalFormat()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.AllowConditionalFormatting, false);
|
|
});
|
|
|
|
var rule = new GreaterThanRule { Value = 10, Format = new Format() };
|
|
var ran = await Run(c,new ConditionalFormatCommand(sheet, RangeRef.Parse("A1"), rule));
|
|
|
|
Assert.False(ran);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DisabledAllowFlag_DoesNotAffectUnrelatedCommand()
|
|
{
|
|
// AllowImages = false should not block a FormatCommand. Independence check.
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.AllowImages, false);
|
|
});
|
|
|
|
var ran = await Run(c,new FormatCommand(sheet, RangeRef.Parse("A1"), new Format { Bold = true }));
|
|
|
|
Assert.True(ran);
|
|
}
|
|
|
|
// ── Undo / Redo gating ──────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task AllowUndoRedo_False_DisablesCanUndoEvenWithHistory()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells["A1"].Value = "hello";
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await Run(c, new ClearContentsCommand(sheet, RangeRef.Parse("A1")));
|
|
Assert.True(c.Instance.CanUndo);
|
|
|
|
c.SetParametersAndRender(p => p.Add(x => x.AllowUndoRedo, false));
|
|
|
|
Assert.False(c.Instance.CanUndo);
|
|
Assert.False(c.Instance.CanRedo);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadOnly_True_DisablesCanUndo()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells["A1"].Value = "hello";
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await Run(c, new ClearContentsCommand(sheet, RangeRef.Parse("A1")));
|
|
Assert.True(c.Instance.CanUndo);
|
|
|
|
c.SetParametersAndRender(p => p.Add(x => x.ReadOnly, true));
|
|
|
|
Assert.False(c.Instance.CanUndo);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Undo_IsNoOp_WhenAllowUndoRedoFalse()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells["A1"].Value = "hello";
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await Run(c, new ClearContentsCommand(sheet, RangeRef.Parse("A1")));
|
|
Assert.Null(sheet.Cells["A1"].Value);
|
|
|
|
c.SetParametersAndRender(p => p.Add(x => x.AllowUndoRedo, false));
|
|
c.Instance.Undo();
|
|
|
|
Assert.Null(sheet.Cells["A1"].Value); // not restored
|
|
}
|
|
|
|
// ── IsFeatureAllowed ────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void IsFeatureAllowed_ReturnsTrue_ByDefault()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, NewWorkbook()));
|
|
|
|
foreach (SpreadsheetFeature f in System.Enum.GetValues(typeof(SpreadsheetFeature)))
|
|
{
|
|
Assert.True(c.Instance.IsFeatureAllowed(f), $"Feature {f} should be allowed by default");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void IsFeatureAllowed_ReadOnly_TurnsOff_AllFeaturesExceptClipboard()
|
|
{
|
|
// Clipboard is intentionally decoupled from ReadOnly so view-only users can
|
|
// still copy data unless the host explicitly sets AllowClipboard to false.
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, NewWorkbook());
|
|
p.Add(x => x.ReadOnly, true);
|
|
});
|
|
|
|
foreach (SpreadsheetFeature f in System.Enum.GetValues(typeof(SpreadsheetFeature)))
|
|
{
|
|
if (f == SpreadsheetFeature.Clipboard)
|
|
{
|
|
Assert.True(c.Instance.IsFeatureAllowed(f), "Clipboard should stay on under ReadOnly");
|
|
}
|
|
else
|
|
{
|
|
Assert.False(c.Instance.IsFeatureAllowed(f), $"Feature {f} should be off in ReadOnly mode");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void IsFeatureAllowed_AllowClipboard_False_TurnsOff_Clipboard_EvenWhenNotReadOnly()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, NewWorkbook());
|
|
p.Add(x => x.AllowClipboard, false);
|
|
});
|
|
|
|
Assert.False(c.Instance.IsFeatureAllowed(SpreadsheetFeature.Clipboard));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsFeatureAllowed_MapsToCorrectAllowParameter()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, NewWorkbook());
|
|
p.Add(x => x.AllowCharts, false);
|
|
});
|
|
|
|
Assert.False(c.Instance.IsFeatureAllowed(SpreadsheetFeature.Charts));
|
|
Assert.True(c.Instance.IsFeatureAllowed(SpreadsheetFeature.Editing));
|
|
Assert.True(c.Instance.IsFeatureAllowed(SpreadsheetFeature.CellFormatting));
|
|
}
|
|
|
|
// ── CommandExecuting / PreventDefault ───────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task CommandExecuting_Fires_BeforeCommandRuns()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells["A1"].Value = "before";
|
|
|
|
ICommand seen = null;
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.CommandExecuting, EventCallbackFactory.Create<SpreadsheetCommandEventArgs>(args =>
|
|
{
|
|
seen = args.Command;
|
|
// When this fires, the command MUST NOT have run yet.
|
|
Assert.Equal("before", sheet.Cells["A1"].Value);
|
|
}));
|
|
});
|
|
|
|
var cmd = new ClearContentsCommand(sheet, RangeRef.Parse("A1"));
|
|
var ran = await Run(c,cmd);
|
|
|
|
Assert.True(ran);
|
|
Assert.Same(cmd, seen);
|
|
Assert.Null(sheet.Cells["A1"].Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PreventDefault_Sync_RejectsCommand()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells["A1"].Value = "keep";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.CommandExecuting, EventCallbackFactory.Create<SpreadsheetCommandEventArgs>(args => args.PreventDefault()));
|
|
});
|
|
|
|
var ran = await Run(c,new ClearContentsCommand(sheet, RangeRef.Parse("A1")));
|
|
|
|
Assert.False(ran);
|
|
Assert.Equal("keep", sheet.Cells["A1"].Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PreventDefault_Async_RejectsCommand()
|
|
{
|
|
// Whole point of the async refactor: PreventDefault must work after awaits.
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells["A1"].Value = "keep";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.CommandExecuting, EventCallbackFactory.Create<SpreadsheetCommandEventArgs>(async args =>
|
|
{
|
|
await Task.Yield();
|
|
args.PreventDefault();
|
|
}));
|
|
});
|
|
|
|
var ran = await Run(c,new ClearContentsCommand(sheet, RangeRef.Parse("A1")));
|
|
|
|
Assert.False(ran);
|
|
Assert.Equal("keep", sheet.Cells["A1"].Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PreventDefault_OnlyForMatchingCommand()
|
|
{
|
|
// Veto FormatCommand but let ClearContentsCommand through.
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells["A1"].Value = "hello";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.CommandExecuting, EventCallbackFactory.Create<SpreadsheetCommandEventArgs>(args =>
|
|
{
|
|
if (args.Command is FormatCommand)
|
|
{
|
|
args.PreventDefault();
|
|
}
|
|
}));
|
|
});
|
|
|
|
var formatRan = await Run(c, new FormatCommand(sheet, RangeRef.Parse("A1"), new Format { Bold = true }));
|
|
var clearRan = await Run(c, new ClearContentsCommand(sheet, RangeRef.Parse("A1")));
|
|
|
|
Assert.False(formatRan);
|
|
Assert.True(clearRan);
|
|
Assert.Null(sheet.Cells["A1"].Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CommandExecuting_NotFired_WhenReadOnly()
|
|
{
|
|
// ReadOnly is the first gate; the event should not fire for rejected commands.
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
|
|
var fired = false;
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.ReadOnly, true);
|
|
p.Add(x => x.CommandExecuting, EventCallbackFactory.Create<SpreadsheetCommandEventArgs>(_ => fired = true));
|
|
});
|
|
|
|
var ran = await Run(c,new ClearContentsCommand(sheet, RangeRef.Parse("A1")));
|
|
|
|
Assert.False(ran);
|
|
Assert.False(fired);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CommandExecuting_NotFired_WhenAllowFlagDisabled()
|
|
{
|
|
// Allow* check happens before the event, same shape as ReadOnly.
|
|
using var ctx = CreateContext();
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
|
|
var fired = false;
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.AllowCharts, false);
|
|
p.Add(x => x.CommandExecuting, EventCallbackFactory.Create<SpreadsheetCommandEventArgs>(_ => fired = true));
|
|
});
|
|
|
|
var chart = new SheetChart { From = new CellAnchor { Row = 0, Column = 0 } };
|
|
var ran = await Run(c,new InsertChartCommand(sheet, chart));
|
|
|
|
Assert.False(ran);
|
|
Assert.False(fired);
|
|
}
|
|
|
|
// ── SelectedSheetIndex (@bind-SelectedSheetIndex) ──────────────────────
|
|
|
|
private static Workbook TwoSheetWorkbook()
|
|
{
|
|
var wb = new Workbook();
|
|
var alpha = wb.AddSheet("Alpha", 10, 10);
|
|
alpha.Cells["A1"].Value = "AlphaCell";
|
|
alpha.Selection.Select(CellRef.Parse("A1"));
|
|
var beta = wb.AddSheet("Beta", 10, 10);
|
|
beta.Cells["A1"].Value = "BetaCell";
|
|
beta.Selection.Select(CellRef.Parse("A1"));
|
|
return wb;
|
|
}
|
|
|
|
[Fact]
|
|
public void SelectedSheetIndex_DefaultsToFirstSheet()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, TwoSheetWorkbook()));
|
|
|
|
Assert.Equal(0, c.Instance.SelectedSheetIndex);
|
|
Assert.Contains("AlphaCell", c.Markup);
|
|
}
|
|
|
|
[Fact]
|
|
public void SelectedSheetIndex_Set_SelectsMatchingSheet()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, TwoSheetWorkbook()));
|
|
|
|
c.SetParametersAndRender(p => p.Add(x => x.SelectedSheetIndex, 1));
|
|
|
|
Assert.Equal(1, c.Instance.SelectedSheetIndex);
|
|
Assert.Contains("BetaCell", c.Markup);
|
|
}
|
|
|
|
[Fact]
|
|
public void SelectedSheetIndex_InitialValue_HonoredWhenWorkbookLoads()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, TwoSheetWorkbook());
|
|
p.Add(x => x.SelectedSheetIndex, 1);
|
|
});
|
|
|
|
Assert.Equal(1, c.Instance.SelectedSheetIndex);
|
|
Assert.Contains("BetaCell", c.Markup);
|
|
}
|
|
|
|
[Fact]
|
|
public void SelectedSheetIndex_OutOfRange_ClampsToLastSheet()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, TwoSheetWorkbook()));
|
|
|
|
c.SetParametersAndRender(p => p.Add(x => x.SelectedSheetIndex, 99));
|
|
|
|
Assert.Contains("BetaCell", c.Markup); // clamped to the last valid sheet
|
|
Assert.DoesNotContain("AlphaCell", c.Markup);
|
|
}
|
|
|
|
[Fact]
|
|
public void SelectedSheetIndex_AddSheet_FiresChangedWithNewIndex()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var reported = new List<int>();
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, TwoSheetWorkbook());
|
|
p.Add(x => x.SelectedSheetIndexChanged, EventCallbackFactory.Create<int>(reported.Add));
|
|
});
|
|
|
|
c.Find(".rz-spreadsheet-add-sheet").Click();
|
|
|
|
Assert.Equal(2, reported[^1]); // the new third sheet becomes active and is reported back
|
|
}
|
|
|
|
// ── Auto fit column width ───────────────────────────────────────────
|
|
|
|
// SetupModule intercepts the InvokeAsync<IJSObjectReference> that creates the Spreadsheet JS
|
|
// object, so measureTexts can return a canned result and its payload can be inspected.
|
|
private static BunitJSModuleInterop SetupSpreadsheetJs(TestContext ctx, params double[] widths)
|
|
{
|
|
var module = ctx.JSInterop.SetupModule("Radzen.createSpreadsheet", _ => true);
|
|
module.Mode = JSRuntimeMode.Loose;
|
|
if (widths.Length > 0)
|
|
{
|
|
module.Setup<double[]>("measureTexts", _ => true).SetResult(widths);
|
|
}
|
|
return module;
|
|
}
|
|
|
|
private static Task AutoFit(IRenderedComponent<RadzenSpreadsheet> c, int column)
|
|
=> c.InvokeAsync(() => c.Instance.OnColumnResizeDoubleClickAsync(new CellEventArgs
|
|
{
|
|
Column = column,
|
|
Pointer = new Microsoft.AspNetCore.Components.Web.PointerEventArgs()
|
|
}));
|
|
|
|
private static List<TextMeasureItem> MeasuredItems(BunitJSModuleInterop module)
|
|
{
|
|
var invocation = Assert.Single(module.Invocations["measureTexts"]);
|
|
return Assert.IsType<List<TextMeasureItem>>(invocation.Arguments[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_ResizesToMeasuredWidth()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var module = SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = "Short";
|
|
sheet.Cells[1, 0].Value = "This is a much longer string";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
|
|
Assert.Equal(300, sheet.Columns[0]);
|
|
Assert.True(sheet.Columns.IsAutoFit(0));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_MeasuresDisplayedValue_NotFormulaText()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var module = SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[1, 0].Value = 1d;
|
|
sheet.Cells[2, 0].Value = 2d;
|
|
sheet.Cells[0, 0].Formula = "=A2+A3";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
|
|
var texts = MeasuredItems(module).Select(item => item.Text).ToList();
|
|
Assert.Contains("3", texts);
|
|
Assert.DoesNotContain("=A2+A3", texts);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_MeasuresNumberFormattedText()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var module = SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = 39448d;
|
|
sheet.Cells[0, 0].Format.NumberFormat = "0.00";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p
|
|
.Add(x => x.Workbook, wb)
|
|
.Add(x => x.Culture, CultureInfo.InvariantCulture));
|
|
|
|
await AutoFit(c, 0);
|
|
|
|
var texts = MeasuredItems(module).Select(item => item.Text).ToList();
|
|
Assert.Contains("39448.00", texts);
|
|
Assert.DoesNotContain("39448", texts);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_EmptyColumn_KeepsWidth()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var module = SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Columns[1] = 50;
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 1);
|
|
|
|
Assert.Equal(50, sheet.Columns[1]);
|
|
Assert.False(sheet.Columns.IsAutoFit(1));
|
|
module.VerifyNotInvoke("measureTexts");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_IgnoresMergedCells()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var module = SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = "extremely long merged content that spans two columns";
|
|
sheet.MergedCells.Add(new RangeRef(new CellRef(0, 0), new CellRef(0, 1)));
|
|
sheet.Cells[1, 0].Value = "tiny";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
|
|
var texts = MeasuredItems(module).Select(item => item.Text).ToList();
|
|
Assert.Equal(["tiny"], texts);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_IncludesHiddenRows()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var module = SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = "short";
|
|
sheet.Cells[1, 0].Value = "very long content in a hidden row";
|
|
sheet.Rows.Hide(1);
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
|
|
var texts = MeasuredItems(module).Select(item => item.Text).ToList();
|
|
Assert.Contains("very long content in a hidden row", texts);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_ClampsToMaxAndMinWidth()
|
|
{
|
|
using var ctx = CreateContext();
|
|
SetupSpreadsheetJs(ctx, 50000);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = "content";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
Assert.Equal(ColumnWidthConversion.MaxWidthInPixels, sheet.Columns[0]);
|
|
|
|
using var ctx2 = CreateContext();
|
|
SetupSpreadsheetJs(ctx2, 5);
|
|
var wb2 = NewWorkbook();
|
|
var sheet2 = wb2.Sheets[0];
|
|
sheet2.Cells[0, 0].Value = "content";
|
|
|
|
var c2 = ctx2.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb2));
|
|
|
|
await AutoFit(c2, 0);
|
|
Assert.Equal(RadzenSpreadsheet.MinColumnWidth, sheet2.Columns[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_Undo_RestoresWidthAndFlag()
|
|
{
|
|
using var ctx = CreateContext();
|
|
SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = "content";
|
|
sheet.Columns[0] = 120;
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
Assert.Equal(300, sheet.Columns[0]);
|
|
Assert.True(sheet.Columns.IsAutoFit(0));
|
|
|
|
await c.InvokeAsync(c.Instance.Undo);
|
|
|
|
Assert.Equal(120, sheet.Columns[0]);
|
|
Assert.False(sheet.Columns.IsAutoFit(0));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_ManualResize_ClearsFlag()
|
|
{
|
|
using var ctx = CreateContext();
|
|
SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = "content";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
Assert.True(sheet.Columns.IsAutoFit(0));
|
|
|
|
await Run(c, new ResizeColumnCommand(sheet, 0, 300, 150));
|
|
|
|
Assert.Equal(150, sheet.Columns[0]);
|
|
Assert.False(sheet.Columns.IsAutoFit(0));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_ResizingDisallowed_DoesNothing()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var module = SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = "content";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
|
{
|
|
p.Add(x => x.Workbook, wb);
|
|
p.Add(x => x.AllowResizing, false);
|
|
});
|
|
|
|
await AutoFit(c, 0);
|
|
|
|
Assert.Equal(100, sheet.Columns[0]);
|
|
module.VerifyNotInvoke("measureTexts");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_DeduplicatesIdenticalCells()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var module = SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = "Same";
|
|
sheet.Cells[1, 0].Value = "Same";
|
|
sheet.Cells[2, 0].Value = "Same";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
|
|
Assert.Single(MeasuredItems(module));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_NullMeasurement_DoesNothing()
|
|
{
|
|
using var ctx = CreateContext();
|
|
SetupSpreadsheetJs(ctx);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = "content";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
|
|
Assert.Equal(100, sheet.Columns[0]);
|
|
Assert.False(sheet.Columns.IsAutoFit(0));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_OutOfRangeOrHiddenColumn_DoesNothing()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var module = SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 2].Value = "content";
|
|
sheet.Columns.Hide(2);
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, -1);
|
|
await AutoFit(c, sheet.Columns.Count);
|
|
await AutoFit(c, 2);
|
|
|
|
Assert.Equal(100, sheet.Columns[2]);
|
|
Assert.False(sheet.Columns.IsAutoFit(2));
|
|
module.VerifyNotInvoke("measureTexts");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_ConditionalFormat_AffectsFontAndSkipsDedupe()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var module = SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = 5d;
|
|
sheet.Cells[5, 0].Value = 5d; // identical twin outside the rule range
|
|
sheet.ConditionalFormats.Add(new RangeRef(new CellRef(0, 0), new CellRef(1, 0)),
|
|
new GreaterThanRule { Value = 0, Format = new Format { Bold = true } });
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
|
|
var items = MeasuredItems(module);
|
|
Assert.Contains(items, item => item.Text == "5" && item.Bold);
|
|
Assert.Contains(items, item => item.Text == "5" && !item.Bold);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_WrapText_MeasuresLongestLine()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var module = SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = "short\nthe longest line here";
|
|
sheet.Cells[0, 0].Format.WrapText = true;
|
|
sheet.Cells[1, 0].Value = "a\nbb";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
|
|
var texts = MeasuredItems(module).Select(item => item.Text).ToList();
|
|
Assert.Contains("the longest line here", texts);
|
|
Assert.Contains("a bb", texts);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_TruncatesAndCapsPayload()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var module = SetupSpreadsheetJs(ctx, 300);
|
|
var wb = new Workbook();
|
|
var sheet = wb.AddSheet("Sheet1", 250, 5);
|
|
sheet.Cells[0, 0].Value = new string('A', 1500);
|
|
for (var row = 1; row < 220; row++)
|
|
{
|
|
sheet.Cells[row, 0].Value = $"item {row}";
|
|
sheet.Cells[row, 0].Format.FontSize = row + 1d; // distinct font group per cell
|
|
}
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
|
|
var items = MeasuredItems(module);
|
|
Assert.Equal(1000, items.Max(item => item.Text.Length));
|
|
Assert.True(items.Count <= 200);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_PoisonCell_IsSkipped()
|
|
{
|
|
using var ctx = CreateContext();
|
|
var module = SetupSpreadsheetJs(ctx, 300);
|
|
var wb = new Workbook();
|
|
var sheet = wb.AddSheet("Sheet1", 200, 5);
|
|
sheet.Cells[0, 0].Value = "good";
|
|
// Out-of-range date serial in a row outside the rendered viewport
|
|
sheet.Cells[150, 0].Value = 1e10;
|
|
sheet.Cells[150, 0].Format.NumberFormat = "dddd, mmmm dd, yyyy";
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
|
|
var texts = MeasuredItems(module).Select(item => item.Text).ToList();
|
|
Assert.Contains("good", texts);
|
|
Assert.Equal(300, sheet.Columns[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_AlreadyAtFitWidth_StillSetsFlag()
|
|
{
|
|
using var ctx = CreateContext();
|
|
SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = "content";
|
|
sheet.Columns[0] = 300;
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
Assert.Equal(300, sheet.Columns[0]);
|
|
Assert.True(sheet.Columns.IsAutoFit(0));
|
|
|
|
await c.InvokeAsync(c.Instance.Undo);
|
|
Assert.False(sheet.Columns.IsAutoFit(0));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutoFitColumn_Top10RuleOverSparseColumn_DoesNotThrow()
|
|
{
|
|
using var ctx = CreateContext();
|
|
SetupSpreadsheetJs(ctx, 300);
|
|
var wb = NewWorkbook();
|
|
var sheet = wb.Sheets[0];
|
|
sheet.Cells[0, 0].Value = 1d;
|
|
sheet.Cells[1, 0].Value = 2d;
|
|
sheet.ConditionalFormats.Add(new RangeRef(new CellRef(0, 0), new CellRef(9, 0)),
|
|
new Top10Rule { Count = 1, Format = new Format { Bold = true } });
|
|
|
|
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
|
|
|
await AutoFit(c, 0);
|
|
|
|
Assert.Equal(300, sheet.Columns[0]);
|
|
}
|
|
|
|
private static class EventCallbackFactory
|
|
{
|
|
// Bunit doesn't expose Microsoft.AspNetCore.Components.EventCallbackFactory directly,
|
|
// so we wrap the typed factory with a tiny helper for readability.
|
|
public static Microsoft.AspNetCore.Components.EventCallback<T> Create<T>(System.Action<T> handler)
|
|
=> new Microsoft.AspNetCore.Components.EventCallbackFactory().Create<T>(new object(), handler);
|
|
|
|
public static Microsoft.AspNetCore.Components.EventCallback<T> Create<T>(System.Func<T, Task> handler)
|
|
=> new Microsoft.AspNetCore.Components.EventCallbackFactory().Create<T>(new object(), handler);
|
|
}
|
|
}
|