feat: implement board tab with dialogs, virtualized card lists and refresh-after-write
- board: columns in id order, per-column sort via config, column width presets (240/320/480) and custom 160-720 - column header new-card and edit/delete menu; card menu delete and move-to-column - card editor as one unit of work with dirty check and discard confirmation; nested tag picker writes new tags immediately - schema constraint violations mapped to typed exceptions and shown inside the modal - dialog service with owner stack so nested modals center on their parent and get FlowDirection - repository: filtered GetCards, CountCards; view-model tests
This commit is contained in:
1 parent
84d08c6768
commit
e3f58ce150
41 files changed
+3004
-26
No files matched your search
@@ -1,5 +1,7 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using YKanBan.Models;
|
||||
using YKanBan.Search;
|
||||
using YKanBan.Storage;
|
||||
using YKanBan.Storage.Workspace;
|
||||
using YKanBan.Tests.TestUtilities;
|
||||
|
||||
@@ -253,8 +255,87 @@ public class WorkspaceRepositoryTests
|
||||
public void AddTagWithDuplicateNameIsRejected()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
Assert.ThrowsExactly<SqliteException>(() =>
|
||||
fixture.Repository.AddTag("bug", RgbColor.Parse("#123456"), ""));
|
||||
DuplicateNameException exception = Assert.ThrowsExactly<DuplicateNameException>(() =>
|
||||
fixture.Repository.AddTag("BUG", RgbColor.Parse("#123456"), ""));
|
||||
Assert.AreEqual(NamedEntity.Tag, exception.Entity);
|
||||
Assert.AreEqual("BUG", exception.Name);
|
||||
Assert.AreEqual(2, fixture.Repository.GetTags().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AddTagWithEmptyNameIsRejected()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
NameRequiredException exception = Assert.ThrowsExactly<NameRequiredException>(() =>
|
||||
fixture.Repository.AddTag("", RgbColor.Parse("#123456"), ""));
|
||||
Assert.AreEqual(NamedEntity.Tag, exception.Entity);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateTagToExistingNameIsRejectedAndKeepsTheRow()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
Assert.ThrowsExactly<DuplicateNameException>(() =>
|
||||
fixture.Repository.UpdateTag(fixture.UiTagId, "Bug", RgbColor.Parse("#00ff00"), ""));
|
||||
Assert.AreEqual("ui", fixture.Repository.GetTags().Single(tag => tag.Id == fixture.UiTagId).Name);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AddColumnWithDuplicateTitleIsRejectedCaseInsensitively()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
DuplicateNameException exception = Assert.ThrowsExactly<DuplicateNameException>(() =>
|
||||
fixture.Repository.AddColumn("COL", ""));
|
||||
Assert.AreEqual(NamedEntity.Column, exception.Entity);
|
||||
Assert.AreEqual(1, fixture.Repository.GetColumns().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AddColumnWithEmptyTitleIsRejected()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
NameRequiredException exception = Assert.ThrowsExactly<NameRequiredException>(() =>
|
||||
fixture.Repository.AddColumn("", "description"));
|
||||
Assert.AreEqual(NamedEntity.Column, exception.Entity);
|
||||
Assert.AreEqual(1, fixture.Repository.GetColumns().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateColumnConstraintViolationsAreTyped()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
ColumnModel other = fixture.Repository.AddColumn("Other", "");
|
||||
|
||||
Assert.ThrowsExactly<DuplicateNameException>(() => fixture.Repository.UpdateColumn(other.Id, "col", ""));
|
||||
Assert.ThrowsExactly<NameRequiredException>(() => fixture.Repository.UpdateColumn(other.Id, "", ""));
|
||||
Assert.AreEqual("Other", fixture.Repository.GetColumn(other.Id)!.Title);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetCardsWithFilterReturnsMatchesWithAllOfTheirTags()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
fixture.Repository.AddCard(fixture.ColumnId, "Other", "", []);
|
||||
|
||||
// Filtering by one tag must still load both tags of the matching card.
|
||||
CompiledSearch filter = SearchSqlCompiler.Compile(SearchQueryParser.Parse("tag:ui")!);
|
||||
IReadOnlyList<CardModel> cards = fixture.Repository.GetCards(filter);
|
||||
|
||||
Assert.AreEqual(1, cards.Count);
|
||||
Assert.AreEqual(fixture.CardId, cards[0].Id);
|
||||
CollectionAssert.AreEqual(new[] { "bug", "ui" }, cards[0].Tags.Select(tag => tag.Name).ToArray());
|
||||
Assert.AreEqual(2, fixture.Repository.GetCards(null).Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CountCardsCountsPerColumn()
|
||||
{
|
||||
using var fixture = new RepositoryFixture();
|
||||
ColumnModel empty = fixture.Repository.AddColumn("Empty", "");
|
||||
fixture.Repository.AddCard(fixture.ColumnId, "", "", []);
|
||||
|
||||
Assert.AreEqual(2L, fixture.Repository.CountCards(fixture.ColumnId));
|
||||
Assert.AreEqual(0L, fixture.Repository.CountCards(empty.Id));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
using YKanBan.Models;
|
||||
using YKanBan.Search;
|
||||
using YKanBan.Storage;
|
||||
using YKanBan.Storage.AppData;
|
||||
using YKanBan.Storage.Workspace;
|
||||
using YKanBan.Tests.TestUtilities;
|
||||
using YKanBan.ViewModels.Board;
|
||||
using YKanBan.ViewModels.Dialogs;
|
||||
|
||||
namespace YKanBan.Tests.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View-model tests of the board and its dialogs against a real temporary
|
||||
/// workspace, with a scripted dialog service standing in for the modal windows.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class BoardViewModelTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Records every dialog shown and lets a test script answer it. The result
|
||||
/// is whatever the dialog closed with; a dialog the script leaves open
|
||||
/// counts as dismissed.
|
||||
/// </summary>
|
||||
private sealed class ScriptedDialogService : IDialogService
|
||||
{
|
||||
public List<DialogViewModelBase> Shown { get; } = [];
|
||||
|
||||
public Func<DialogViewModelBase, Task> Script { get; set; } = _ => Task.CompletedTask;
|
||||
|
||||
public async Task<bool> ShowAsync(DialogViewModelBase dialog)
|
||||
{
|
||||
Shown.Add(dialog);
|
||||
bool? result = null;
|
||||
dialog.CloseRequested += (_, closed) => result ??= closed;
|
||||
await Script(dialog);
|
||||
return result ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Answers every confirmation dialog with the given choice.</summary>
|
||||
private static Func<DialogViewModelBase, Task> AnswerConfirmations(bool confirm) => dialog =>
|
||||
{
|
||||
if (dialog is ConfirmDialogViewModel confirmation)
|
||||
{
|
||||
if (confirm)
|
||||
{
|
||||
confirmation.ConfirmCommand.Execute(null);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
return confirmation.CancelCommand.ExecuteAsync(null);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
private sealed class Fixture : IDisposable
|
||||
{
|
||||
public TempDirectory Directory { get; } = new();
|
||||
|
||||
public WorkspaceRepository Repository { get; }
|
||||
|
||||
public AppConfig Config { get; } = new();
|
||||
|
||||
public ScriptedDialogService Dialogs { get; } = new();
|
||||
|
||||
public Fixture()
|
||||
{
|
||||
WorkspaceInitializer.Initialize(Directory.FullPath);
|
||||
Repository = new WorkspaceRepository(Directory.FullPath);
|
||||
}
|
||||
|
||||
public BoardViewModel CreateBoard() => new(Repository, Config, Dialogs);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Repository.Dispose();
|
||||
Directory.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RefreshListsColumnsInIdOrderAndSortsCardsWithinEachColumn()
|
||||
{
|
||||
using var fixture = new Fixture();
|
||||
ColumnModel first = fixture.Repository.AddColumn("A", "");
|
||||
ColumnModel second = fixture.Repository.AddColumn("B", "");
|
||||
fixture.Repository.AddCard(first.Id, "zeta", "", []);
|
||||
fixture.Repository.AddCard(first.Id, "alpha", "", []);
|
||||
fixture.Repository.AddCard(second.Id, "x", "", []);
|
||||
|
||||
fixture.Config.Sort.Card = CardSortOption.Title;
|
||||
BoardViewModel board = fixture.CreateBoard();
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "A", "B" }, board.Columns.Select(column => column.Title).ToArray());
|
||||
CollectionAssert.AreEqual(new[] { "alpha", "zeta" }, board.Columns[0].Cards.Select(card => card.Title).ToArray());
|
||||
Assert.AreEqual(1, board.Columns[1].CardCount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ChangingTheSortPersistsItToTheConfigurationAndResorts()
|
||||
{
|
||||
using var fixture = new Fixture();
|
||||
ColumnModel column = fixture.Repository.AddColumn("A", "");
|
||||
fixture.Repository.AddCard(column.Id, "b", "", []);
|
||||
fixture.Repository.AddCard(column.Id, "a", "", []);
|
||||
BoardViewModel board = fixture.CreateBoard();
|
||||
CollectionAssert.AreEqual(new[] { "b", "a" }, board.Columns[0].Cards.Select(card => card.Title).ToArray());
|
||||
|
||||
board.SelectedSort = board.SortOptions.Single(option => (CardSortOption)option.Value == CardSortOption.Title);
|
||||
|
||||
Assert.AreEqual(CardSortOption.Title, fixture.Config.Sort.Card);
|
||||
CollectionAssert.AreEqual(new[] { "a", "b" }, board.Columns[0].Cards.Select(card => card.Title).ToArray());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task WritesRefreshWithTheActiveFilterAndKeepEmptyColumns()
|
||||
{
|
||||
using var fixture = new Fixture();
|
||||
ColumnModel todo = fixture.Repository.AddColumn("Todo", "");
|
||||
ColumnModel done = fixture.Repository.AddColumn("Done", "");
|
||||
CardModel match = fixture.Repository.AddCard(todo.Id, "match me", "", []);
|
||||
fixture.Repository.AddCard(todo.Id, "other", "", []);
|
||||
|
||||
BoardViewModel board = fixture.CreateBoard();
|
||||
board.ActiveFilter = SearchSqlCompiler.Compile(SearchQueryParser.Parse("match")!);
|
||||
board.Refresh();
|
||||
Assert.AreEqual(1, board.Columns[0].CardCount);
|
||||
|
||||
// A move is a write: the board re-queries with the active filter.
|
||||
CardViewModel card = board.Columns[0].Cards.Single();
|
||||
MoveTargetViewModel target = card.MoveTargets.Single(entry => entry.ColumnId == done.Id);
|
||||
Assert.IsFalse(card.MoveTargets.Single(entry => entry.ColumnId == todo.Id).IsEnabled);
|
||||
await target.Command.ExecuteAsync(target.ColumnId);
|
||||
|
||||
Assert.AreEqual(2, board.Columns.Count, "Columns emptied by the filter stay on the board.");
|
||||
Assert.AreEqual(0, board.Columns[0].CardCount);
|
||||
Assert.AreEqual(match.Id, board.Columns[1].Cards.Single().Card.Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task DeclinedColumnDeletionKeepsTheColumnAndShowsItsCardCount()
|
||||
{
|
||||
using var fixture = new Fixture();
|
||||
ColumnModel column = fixture.Repository.AddColumn("A", "");
|
||||
fixture.Repository.AddCard(column.Id, "", "", []);
|
||||
fixture.Repository.AddCard(column.Id, "", "", []);
|
||||
BoardViewModel board = fixture.CreateBoard();
|
||||
fixture.Dialogs.Script = AnswerConfirmations(false);
|
||||
|
||||
await board.Columns[0].DeleteCommand.ExecuteAsync(null);
|
||||
|
||||
var confirmation = (ConfirmDialogViewModel)fixture.Dialogs.Shown.Single();
|
||||
StringAssert.Contains(confirmation.Message, "2");
|
||||
Assert.AreEqual(1, fixture.Repository.GetColumns().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ColumnDeletionWithoutConfirmationDeletesImmediately()
|
||||
{
|
||||
using var fixture = new Fixture();
|
||||
fixture.Repository.AddColumn("A", "");
|
||||
fixture.Config.Confirmations.DeleteColumn = false;
|
||||
BoardViewModel board = fixture.CreateBoard();
|
||||
|
||||
await board.Columns[0].DeleteCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.AreEqual(0, fixture.Dialogs.Shown.Count);
|
||||
Assert.AreEqual(0, board.Columns.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ConfirmedCardDeletionDeletesTheCard()
|
||||
{
|
||||
using var fixture = new Fixture();
|
||||
ColumnModel column = fixture.Repository.AddColumn("A", "");
|
||||
fixture.Repository.AddCard(column.Id, "gone", "", []);
|
||||
BoardViewModel board = fixture.CreateBoard();
|
||||
fixture.Dialogs.Script = AnswerConfirmations(true);
|
||||
|
||||
await board.Columns[0].Cards[0].DeleteCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.AreEqual(0, board.Columns[0].CardCount);
|
||||
Assert.AreEqual(0, fixture.Repository.GetCards().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task DuplicateColumnTitleIsReportedInsideTheDialog()
|
||||
{
|
||||
using var fixture = new Fixture();
|
||||
fixture.Repository.AddColumn("Todo", "");
|
||||
BoardViewModel board = fixture.CreateBoard();
|
||||
ColumnEditorDialogViewModel? editor = null;
|
||||
bool closed = false;
|
||||
fixture.Dialogs.Script = dialog =>
|
||||
{
|
||||
editor = (ColumnEditorDialogViewModel)dialog;
|
||||
editor.CloseRequested += (_, _) => closed = true;
|
||||
editor.ColumnTitle = "TODO";
|
||||
editor.ConfirmCommand.Execute(null);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
await board.AddColumnCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.IsNotNull(editor);
|
||||
Assert.IsFalse(closed, "The dialog stays open on a constraint violation.");
|
||||
Assert.IsTrue(editor.HasError);
|
||||
StringAssert.Contains(editor.ErrorMessage, "TODO");
|
||||
Assert.AreEqual(1, fixture.Repository.GetColumns().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task NewCardIsWrittenWithItsTagsOnConfirmation()
|
||||
{
|
||||
using var fixture = new Fixture();
|
||||
ColumnModel column = fixture.Repository.AddColumn("A", "");
|
||||
TagModel bug = fixture.Repository.AddTag("bug", new RgbColor(255, 0, 0), "");
|
||||
BoardViewModel board = fixture.CreateBoard();
|
||||
fixture.Dialogs.Script = async dialog =>
|
||||
{
|
||||
switch (dialog)
|
||||
{
|
||||
case CardEditorDialogViewModel editor:
|
||||
editor.CardTitle = "Fix it";
|
||||
editor.Content = "Details";
|
||||
await editor.AddTagCommand.ExecuteAsync(null);
|
||||
editor.ConfirmCommand.Execute(null);
|
||||
break;
|
||||
case TagPickerDialogViewModel picker:
|
||||
picker.PickCommand.Execute(picker.FilteredTags.Single());
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
await board.Columns[0].AddCardCommand.ExecuteAsync(null);
|
||||
|
||||
CardModel card = fixture.Repository.GetCards().Single();
|
||||
Assert.AreEqual("Fix it", card.Title);
|
||||
Assert.AreEqual("Details", card.Content);
|
||||
Assert.AreEqual(bug.Id, card.Tags.Single().Id);
|
||||
Assert.AreEqual(card.Id, board.Columns[0].Cards.Single().Card.Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UnchangedEditConfirmationWritesNothing()
|
||||
{
|
||||
using var fixture = new Fixture();
|
||||
ColumnModel column = fixture.Repository.AddColumn("A", "");
|
||||
CardModel card = fixture.Repository.AddCard(column.Id, "t", "c", []);
|
||||
using (var connection = SqliteTestHelper.OpenWorkspace(fixture.Directory.FullPath))
|
||||
{
|
||||
SqliteTestHelper.Exec(connection, "UPDATE cards SET updated_at = 100;");
|
||||
}
|
||||
card = fixture.Repository.GetCard(card.Id)!;
|
||||
|
||||
var editor = CardEditorDialogViewModel.ForExistingCard(fixture.Repository, fixture.Dialogs, card, confirmDiscard: true);
|
||||
editor.CardTitle = "changed";
|
||||
editor.CardTitle = "t";
|
||||
Assert.IsFalse(editor.IsDirty);
|
||||
editor.ConfirmCommand.Execute(null);
|
||||
|
||||
Assert.AreEqual(100, fixture.Repository.GetCard(card.Id)!.UpdatedAt);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task DirtyCancelAsksAndStaysOpenWhenDeclined()
|
||||
{
|
||||
using var fixture = new Fixture();
|
||||
ColumnModel column = fixture.Repository.AddColumn("A", "");
|
||||
var editor = CardEditorDialogViewModel.ForNewCard(fixture.Repository, fixture.Dialogs, column.Id, confirmDiscard: true);
|
||||
bool? closedWith = null;
|
||||
editor.CloseRequested += (_, result) => closedWith = result;
|
||||
editor.Content = "draft";
|
||||
|
||||
fixture.Dialogs.Script = AnswerConfirmations(false);
|
||||
await editor.RequestCancelAsync();
|
||||
Assert.IsNull(closedWith, "Declining the discard keeps the editor open.");
|
||||
|
||||
fixture.Dialogs.Script = AnswerConfirmations(true);
|
||||
await editor.RequestCancelAsync();
|
||||
Assert.IsFalse(closedWith);
|
||||
Assert.AreEqual(0, fixture.Repository.GetCards().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CleanCancelClosesWithoutAsking()
|
||||
{
|
||||
using var fixture = new Fixture();
|
||||
ColumnModel column = fixture.Repository.AddColumn("A", "");
|
||||
var editor = CardEditorDialogViewModel.ForNewCard(fixture.Repository, fixture.Dialogs, column.Id, confirmDiscard: true);
|
||||
bool? closedWith = null;
|
||||
editor.CloseRequested += (_, result) => closedWith = result;
|
||||
|
||||
await editor.RequestCancelAsync();
|
||||
|
||||
Assert.IsFalse(closedWith);
|
||||
Assert.AreEqual(0, fixture.Dialogs.Shown.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task TagCreatedInThePickerSurvivesACancelledCardEdit()
|
||||
{
|
||||
using var fixture = new Fixture();
|
||||
ColumnModel column = fixture.Repository.AddColumn("A", "");
|
||||
var editor = CardEditorDialogViewModel.ForNewCard(fixture.Repository, fixture.Dialogs, column.Id, confirmDiscard: false);
|
||||
fixture.Dialogs.Script = dialog =>
|
||||
{
|
||||
if (dialog is TagPickerDialogViewModel picker)
|
||||
{
|
||||
picker.NewName = "fresh";
|
||||
picker.NewColor = "#123456";
|
||||
picker.CreateCommand.Execute(null);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
await editor.AddTagCommand.ExecuteAsync(null);
|
||||
Assert.AreEqual("fresh", editor.Tags.Single().Name);
|
||||
await editor.RequestCancelAsync();
|
||||
|
||||
Assert.AreEqual("fresh", fixture.Repository.GetTags().Single().Name);
|
||||
Assert.AreEqual(0, fixture.Repository.GetCards().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TagPickerReportsInvalidColorAndDuplicateNameWithoutClosing()
|
||||
{
|
||||
using var fixture = new Fixture();
|
||||
fixture.Repository.AddTag("bug", new RgbColor(1, 2, 3), "");
|
||||
var picker = new TagPickerDialogViewModel(fixture.Repository, []);
|
||||
bool closed = false;
|
||||
picker.CloseRequested += (_, _) => closed = true;
|
||||
|
||||
picker.NewName = "new";
|
||||
picker.NewColor = "red";
|
||||
picker.CreateCommand.Execute(null);
|
||||
Assert.AreEqual(YKanBan.Resources.Error_Tag_InvalidColor, picker.ErrorMessage);
|
||||
|
||||
picker.NewName = "BUG";
|
||||
picker.NewColor = "#ff0000";
|
||||
picker.CreateCommand.Execute(null);
|
||||
StringAssert.Contains(picker.ErrorMessage, "BUG");
|
||||
|
||||
Assert.IsFalse(closed);
|
||||
Assert.AreEqual(1, fixture.Repository.GetTags().Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TagPickerHidesAssignedTagsAndFiltersCaseInsensitively()
|
||||
{
|
||||
using var fixture = new Fixture();
|
||||
TagModel bug = fixture.Repository.AddTag("Bug", new RgbColor(1, 2, 3), "");
|
||||
fixture.Repository.AddTag("UI", new RgbColor(1, 2, 3), "");
|
||||
fixture.Repository.AddTag("Build", new RgbColor(1, 2, 3), "");
|
||||
var picker = new TagPickerDialogViewModel(fixture.Repository, [bug.Id]);
|
||||
|
||||
picker.FilterText = "bu";
|
||||
CollectionAssert.AreEqual(new[] { "Build" }, picker.FilteredTags.Select(tag => tag.Name).ToArray());
|
||||
|
||||
picker.FilterText = "zzz";
|
||||
Assert.IsTrue(picker.NoMatches);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ColumnWidthDialogCommitsPresetsAndClampsCustomPixels()
|
||||
{
|
||||
var settings = new ColumnWidthSettings();
|
||||
var dialog = new ColumnWidthDialogViewModel(settings) { IsStandard = false, IsCustom = true, CustomPixels = 5000 };
|
||||
dialog.ConfirmCommand.Execute(null);
|
||||
Assert.AreEqual(ColumnWidthPreset.Custom, settings.Preset);
|
||||
Assert.AreEqual(AppConfig.MaxColumnPixels, settings.CustomPixels);
|
||||
Assert.AreEqual(AppConfig.MaxColumnPixels, ColumnWidths.Resolve(settings));
|
||||
|
||||
dialog = new ColumnWidthDialogViewModel(settings) { IsCustom = false, IsWide = true };
|
||||
dialog.ConfirmCommand.Execute(null);
|
||||
Assert.AreEqual(ColumnWidthPreset.Wide, settings.Preset);
|
||||
Assert.AreEqual(ColumnWidths.Wide, ColumnWidths.Resolve(settings));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CardHeaderAndExcerptFollowTheDisplayRules()
|
||||
{
|
||||
var untitled = new CardModel(61, 1, "", "", 0, 0, []);
|
||||
var titled = untitled with { Title = "Foo bar" };
|
||||
Assert.AreEqual("#61", CardViewModel.FormatHeader(untitled));
|
||||
Assert.AreEqual("#61 Foo bar", CardViewModel.FormatHeader(titled));
|
||||
|
||||
Assert.AreEqual("short", CardViewModel.MakeExcerpt(" short \n"));
|
||||
string longContent = string.Concat(Enumerable.Repeat("😀", CardViewModel.ExcerptLength + 5));
|
||||
string excerpt = CardViewModel.MakeExcerpt(longContent);
|
||||
Assert.IsTrue(excerpt.EndsWith('…'));
|
||||
Assert.AreEqual((CardViewModel.ExcerptLength * 2) + 1, excerpt.Length, "Surrogate pairs are kept whole.");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user