Files
YKanBan/YKanBan.Tests/Export/MarkdownExporterTests.cs
T
yyc12345 b5614ff59b feat(m3): search and export
- Pidgin lexer: words, escaped phrases (strict closing), parens
- Full-Pidgin token-level parser, strict: malformed input throws
  SearchSyntaxException (BREAKING: lenient degradation removed from
  the plan; M7 must surface the error, M8 USAGE.md documents strict
  behavior)
- AST with reference-equality collection terms; structural
  comparison lives in the test-side SearchNodeComparer (YYC MARK
  note records the record-equality pitfall)
- SQL compiler: parameterized predicate over cards alias, LIKE
  escaping, COLLATE NOCASE exact matches, tag EXISTS / column IN
  subqueries
- Markdown exporter: ResX-driven templates, per-language export
  time AND card time patterns, kebab i18n file names, table cell
  escaping
- New RgbColor value type (byte components, strict #RRGGBB parse,
  canonical lowercase text) wired through TagModel and repository
- 57 new tests (grammar branches, strict errors, SQL end-to-end,
  en/zh export snapshots, color conversions)
2026-10-01 22:20:56 +08:00

172 lines
5.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Globalization;
using YKanBan.Export;
using YKanBan.Models;
namespace YKanBan.Tests.Export;
/// <summary>
/// Tests for the Markdown exporter. Mutates the global Resources.Culture,
/// therefore marked DoNotParallelize. Asserts real localized output for
/// both supported languages.
/// </summary>
[TestClass]
[DoNotParallelize]
public class MarkdownExporterTests
{
private static readonly DateTimeOffset ExportedAt = new(2026, 9, 30, 14, 25, 37, TimeSpan.Zero);
/// <summary>A local wall-clock moment, matching the exporter's conversion path.</summary>
private static DateTimeOffset Local(int year, int month, int day, int hour, int minute) =>
new DateTimeOffset(new DateTime(year, month, day, hour, minute, 0)).ToLocalTime();
/// <summary>Unix seconds for a local wall-clock time.</summary>
private static long UnixTimeOf(int year, int month, int day, int hour, int minute) =>
Local(year, month, day, hour, minute).ToUnixTimeSeconds();
/// <summary>Builds the board fixture: one working column with two cards, one empty column, two tags.</summary>
private static (IReadOnlyList<ColumnModel> Columns, IReadOnlyList<CardModel> Cards, IReadOnlyList<(TagModel, long)> Tags) CreateBoard()
{
long created = UnixTimeOf(2026, 9, 1, 10, 0);
long updated = UnixTimeOf(2026, 9, 28, 18, 30);
var columns = new List<ColumnModel>
{
new(1, "In Progress", "The work column", 100, 100),
new(2, "Empty", string.Empty, 100, 100),
};
var bug = new TagModel(1, "bug", RgbColor.Parse("#ff0000"), string.Empty);
var ui = new TagModel(2, "ui", RgbColor.Parse("#00ff00"), "Interface work");
var cards = new List<CardModel>
{
new(61, 1, "Fix login", "Body text", created, updated, [bug, ui]),
new(62, 1, string.Empty, string.Empty, created, updated, []),
};
var tags = new List<(TagModel, long)> { (bug, 1), (ui, 1) };
return (columns, cards, tags);
}
private static string Export(CultureInfo culture)
{
CultureInfo? original = Resources.Culture;
try
{
Resources.Culture = culture;
(IReadOnlyList<ColumnModel> columns, IReadOnlyList<CardModel> cards, IReadOnlyList<(TagModel, long)> tags) = CreateBoard();
return MarkdownExporter.Export("MyRepo", columns, cards, tags, ExportedAt);
}
finally
{
Resources.Culture = original;
}
}
private static string Normalize(string text) => text.Replace("\r\n", "\n");
[TestMethod]
public void EnglishOutputMatchesTemplate()
{
string created = Local(2026, 9, 1, 10, 0).ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
string updated = Local(2026, 9, 28, 18, 30).ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
string expected = Normalize($"""
# MyRepo
> Exported at: 2026-09-30 14:25:37
## Column: In Progress (2)
The work column
### #61 Fix login
Body text
- Tags: `bug` `ui`
- Created: {created} Modified: {updated}
### #62
- Created: {created} Modified: {updated}
## Column: Empty (0)
## Tags
| Name | Color | Description | Usage |
|---|---|---|---|
| bug | #ff0000 | | 1 |
| ui | #00ff00 | Interface work | 1 |
""");
Assert.AreEqual(expected + "\n", Normalize(Export(new CultureInfo("en"))));
}
[TestMethod]
public void ChineseOutputMatchesTemplate()
{
var chinese = new CultureInfo("zh-Hans");
string created = Local(2026, 9, 1, 10, 0).ToString("yyyy年M月d日 HH:mm", chinese);
string updated = Local(2026, 9, 28, 18, 30).ToString("yyyy年M月d日 HH:mm", chinese);
string expected = Normalize($"""
# MyRepo
> 导出时间:2026年9月30日 14:25
## 列:In Progress(2)
The work column
### #61 Fix login
Body text
- 标签:`bug` `ui`
- 创建:{created} 修改:{updated}
### #62
- 创建:{created} 修改:{updated}
## 列:Empty(0)
## 标签总表
| 名称 | 颜色 | 描述 | 使用数 |
|---|---|---|---|
| bug | #ff0000 | | 1 |
| ui | #00ff00 | Interface work | 1 |
""");
Assert.AreEqual(expected + "\n", Normalize(Export(new CultureInfo("zh-Hans"))));
}
[TestMethod]
public void FileNameFollowsLanguage()
{
CultureInfo? original = Resources.Culture;
try
{
Resources.Culture = new CultureInfo("en");
Assert.AreEqual("MyRepo-export-20260930-142537.md", MarkdownExporter.BuildFileName("MyRepo", ExportedAt));
Resources.Culture = new CultureInfo("zh-Hans");
Assert.AreEqual("MyRepo-导出-20260930-142537.md", MarkdownExporter.BuildFileName("MyRepo", ExportedAt));
}
finally
{
Resources.Culture = original;
}
}
[TestMethod]
public void TagTableCellsEscapePipesAndLineBreaks()
{
CultureInfo? original = Resources.Culture;
try
{
Resources.Culture = new CultureInfo("en");
var tag = new TagModel(1, "a|b", RgbColor.Parse("#112233"), "first\nsecond");
string markdown = MarkdownExporter.Export("W",
[new ColumnModel(1, "C", string.Empty, 1, 1)],
[],
[(tag, 0L)],
ExportedAt);
StringAssert.Contains(markdown, "| a\\|b | #112233 | first second | 0 |");
}
finally
{
Resources.Culture = original;
}
}
}