diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1fe7672..1e8aa77 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,6 +3,7 @@
true
+
@@ -10,5 +11,7 @@
+
+
\ No newline at end of file
diff --git a/PowershellModule.slnx b/PowershellModule.slnx
index 858fae2..04cf69b 100644
--- a/PowershellModule.slnx
+++ b/PowershellModule.slnx
@@ -12,6 +12,9 @@
+
+
+
diff --git a/src/ModuleCore/Calendar/Calendar.cs b/src/ModuleCore/Calendar/CalendarGenerator.cs
similarity index 93%
rename from src/ModuleCore/Calendar/Calendar.cs
rename to src/ModuleCore/Calendar/CalendarGenerator.cs
index 019d748..8494fc9 100644
--- a/src/ModuleCore/Calendar/Calendar.cs
+++ b/src/ModuleCore/Calendar/CalendarGenerator.cs
@@ -1,10 +1,9 @@
๏ปฟusing System.Globalization;
-using System.IO.MemoryMappedFiles;
using System.Text;
namespace ModuleCore.Calendar;
-public sealed class Calendar
+public sealed class CalendarGenerator
{
public const string DefaultMarkedOffSymbol = "x";
public const DayOfWeek DefaultStartOfWeek = DayOfWeek.Monday;
@@ -30,7 +29,7 @@ public sealed class Calendar
private readonly string _rolloverDaySymbol = "";
- public Calendar(string? markedDaySymbol = null, DayOfWeek? startOfWeek = null)
+ public CalendarGenerator(string? markedDaySymbol = null, DayOfWeek? startOfWeek = null)
{
_markedOffSymbol = markedDaySymbol ?? DefaultMarkedOffSymbol;
_startOfWeek = startOfWeek ?? DefaultStartOfWeek;
@@ -38,10 +37,7 @@ public sealed class Calendar
// Generate localised day of week lookup table
_daysOfWeekLookup = new List(7);
_dayWidth = _markedOffSymbol.Length;
- // TODO: make this passed in from configuration, and if its invalid default it to
- // CultureInfo.CurrentCulture
- // TODO: test if its possible to determine if a culture string is invalid and what happens
- _cultureInfo = new CultureInfo("en-AU");
+ _cultureInfo = CultureInfo.CurrentCulture;
for (var i = 0; i < 7; i++)
{
var localisedDayAbbreviation = _cultureInfo.DateTimeFormat.GetAbbreviatedDayName((DayOfWeek)i);
@@ -68,13 +64,19 @@ public sealed class Calendar
_dayWidth += 2;
}
- public string RenderCalendar(DateTime month)
+ ///
+ /// Renders the configured calendar for the given datetime
+ ///
+ ///
+ /// Defaults to , used to display elapsed days
+ ///
+ public string Render(DateTime month, DateTime? todayOverride = null)
{
var sb = new StringBuilder();
// Generate the header for the month/days of week
GenerateHeader(sb, month);
- var calendarForMonth = GenerateCalendarForMonth(month, _markedOffSymbol, _startOfWeek, _rolloverDaySymbol);
+ var calendarForMonth = GenerateCalendarForMonth(month, _markedOffSymbol, _startOfWeek, _rolloverDaySymbol, todayOverride ?? DateTime.Now);
for (var i = 0; i < calendarForMonth.WeeksInMonth; i++)
{
@@ -168,9 +170,10 @@ public sealed class Calendar
}
}
- private MonthCalendar GenerateCalendarForMonth(DateTime month, string markedOffSymbol, DayOfWeek calendarStartOfWeek, string rolloverDaySymbol)
+ private MonthCalendar GenerateCalendarForMonth(DateTime month, string markedOffSymbol, DayOfWeek calendarStartOfWeek, string rolloverDaySymbol, DateTime today)
{
// TODO: use startOfWeek to offset the start of the calendar later
+ // TODO: I think I meant the calendar name header?
// Reset the month to the start
var startOfMonth = new DateTime(month.Year, month.Month, 1);
@@ -218,7 +221,6 @@ public sealed class Calendar
cal.Add(rolloverDaySymbol);
}
- var today = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
// If we're in the same year and month, any days already elapsed should be marked off,
// otherwise we display all days.
// If the DateTime we're rendering is in the past we render all days as marked off
diff --git a/src/ModuleHarness/Program.cs b/src/ModuleHarness/Program.cs
index a01a1db..d314f9d 100644
--- a/src/ModuleHarness/Program.cs
+++ b/src/ModuleHarness/Program.cs
@@ -8,10 +8,10 @@ class Program
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.Unicode;
- var a = new Calendar();
- var lastMonth = a.RenderCalendar(DateTime.Now.AddMonths(-1));
- var now = a.RenderCalendar(DateTime.Now);
- var august = a.RenderCalendar(DateTime.Now.AddMonths(1));
+ var a = new CalendarGenerator();
+ var lastMonth = a.Render(DateTime.Now.AddMonths(-1));
+ var now = a.Render(DateTime.Now);
+ var august = a.Render(DateTime.Now.AddMonths(1));
Console.Write(lastMonth);
Console.Write(now);
diff --git a/src/PowershellModule/Calendar/GetCalendarCommand.cs b/src/PowershellModule/Calendar/GetCalendarCommand.cs
index 64a6db7..f70d6e0 100644
--- a/src/PowershellModule/Calendar/GetCalendarCommand.cs
+++ b/src/PowershellModule/Calendar/GetCalendarCommand.cs
@@ -1,6 +1,7 @@
๏ปฟusing System;
using System.Globalization;
using System.Management.Automation;
+using ModuleCore.Calendar;
namespace PowershellModule.Calendar
{
@@ -42,7 +43,7 @@ namespace PowershellModule.Calendar
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
// Guaranteed to be initialised in BeginProcessing. If it's not, something is cooked in some other way
- private ModuleCore.Calendar.Calendar _calendar = null!;
+ private CalendarGenerator _calendar = null!;
private const string Noun = "Calendar";
public const string FullName = $"{VerbsCommon.Get}-{Noun}";
@@ -80,7 +81,7 @@ namespace PowershellModule.Calendar
{
if (dayOfWeek is null)
{
- return ModuleCore.Calendar.Calendar.DefaultStartOfWeek;
+ return CalendarGenerator.DefaultStartOfWeek;
}
return dayOfWeek.ToLowerInvariant() switch
@@ -92,13 +93,13 @@ namespace PowershellModule.Calendar
"thursday" => DayOfWeek.Thursday,
"friday" => DayOfWeek.Friday,
"saturday" => DayOfWeek.Saturday,
- _ => ModuleCore.Calendar.Calendar.DefaultStartOfWeek
+ _ => CalendarGenerator.DefaultStartOfWeek
};
}
protected override void ProcessRecord()
{
- var calendar = _calendar.RenderCalendar(_dateToRender);
+ var calendar = _calendar.Render(_dateToRender);
// From _very_ basic testing, string split allocates the same as making a span and iterating over it.
// I'm assuming this is because a lot of the span code is what string.Split() does internally and the rest
diff --git a/tests/ModuleTests/Calendar/BasicRenderTest.cs b/tests/ModuleTests/Calendar/BasicRenderTest.cs
new file mode 100644
index 0000000..3bc791f
--- /dev/null
+++ b/tests/ModuleTests/Calendar/BasicRenderTest.cs
@@ -0,0 +1,48 @@
+๏ปฟusing ModuleCore.Calendar;
+using ModuleTests.Calendar.TestData;
+using System.Globalization;
+
+namespace ModuleTests.Calendar;
+
+public class BasicRenderTest
+{
+ private static readonly VerifySettings Settings;
+
+ static BasicRenderTest()
+ {
+ Settings = new VerifySettings();
+ var testBaseDirectory = Path.Join(TestConstants.SnapshotFolderName, nameof(BasicRenderTest));
+
+ Settings.UseDirectory(testBaseDirectory);
+ Settings.DisableDiff();
+ }
+
+ [Theory]
+ [ClassData(typeof(CalendarTestDates))]
+ public Task BasicCalendarRender(DateTime month)
+ {
+ Settings.UseFileName($"{month:yyyy-MM-dd}");
+
+ var calendar = new CalendarGenerator();
+ // Render the calendar with the start of the month as the today date so no elapsed marks are shown
+ var renderedMonth = calendar.Render(month, new DateTime(month.Year, month.Month, 1));
+
+ return Verify(renderedMonth, Settings);
+ }
+
+ [Theory]
+ [ClassData(typeof(CultureCodeTestData))]
+ public Task CultureInfoCalendarRender(string cultureCode)
+ {
+ Settings.UseFileName($"{cultureCode}");
+
+ Thread.CurrentThread.CurrentCulture = new CultureInfo(cultureCode);
+
+ var month = new DateTime(2026, 8, 1);
+ var calendar = new CalendarGenerator();
+ // Render the calendar with the start of the month as the today date so no elapsed marks are shown
+ var renderedMonth = calendar.Render(month, new DateTime(month.Year, month.Month, 1));
+
+ return Verify(renderedMonth, Settings);
+ }
+}
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/MarkedDayRenderTests.cs b/tests/ModuleTests/Calendar/MarkedDayRenderTests.cs
new file mode 100644
index 0000000..24af2ce
--- /dev/null
+++ b/tests/ModuleTests/Calendar/MarkedDayRenderTests.cs
@@ -0,0 +1,86 @@
+๏ปฟusing System.Text;
+using ModuleCore.Calendar;
+
+namespace ModuleTests.Calendar;
+
+public class MarkedDayRenderTests
+{
+ private static readonly VerifySettings Settings;
+
+ static MarkedDayRenderTests()
+ {
+ Settings = new VerifySettings();
+ var testBaseDirectory = Path.Join(TestConstants.SnapshotFolderName, nameof(MarkedDayRenderTests));
+
+ Settings.UseDirectory(testBaseDirectory);
+ Settings.DisableDiff();
+ }
+
+ [Fact]
+ public Task DefaultMarkedDayRender()
+ {
+ Settings.UseFileName(nameof(DefaultMarkedDayRender));
+
+ var date = new DateTime(2026, 8, 1);
+ var calendar = new CalendarGenerator();
+ var renderedMonth = calendar.Render(date, date.AddDays(15));
+
+ return Verify(renderedMonth, Settings);
+ }
+
+ [Fact]
+ public Task SimpleMarkedDayRender()
+ {
+ Settings.UseFileName(nameof(SimpleMarkedDayRender));
+
+ var date = new DateTime(2026, 8, 1);
+ var calendar = new CalendarGenerator("|");
+ var renderedMonth = calendar.Render(date, date.AddDays(15));
+
+ return Verify(renderedMonth, Settings);
+ }
+
+ [Fact]
+ public Task LongMarkedDayRender()
+ {
+ Settings.UseFileName(nameof(LongMarkedDayRender));
+
+ var date = new DateTime(2026, 8, 1);
+ var calendar = new CalendarGenerator("passed");
+ var renderedMonth = calendar.Render(date, date.AddDays(15));
+
+ return Verify(renderedMonth, Settings);
+ }
+
+ [Fact]
+ public Task EmojiMarkedDayRender()
+ {
+ Settings.UseFileName(nameof(EmojiMarkedDayRender));
+
+ var date = new DateTime(2026, 8, 1);
+ var calendar = new CalendarGenerator("๐คซ");
+ var sb = new StringBuilder();
+ sb.AppendLine("Emojis will display incorrectly in text but output correctly in terminals with UTF-8 encoding.")
+ .AppendLine(calendar.Render(date, date.AddDays(15)));
+
+ return Verify(sb, Settings);
+ }
+
+ [Fact]
+ public Task DoubleWideMarkedDayRender()
+ {
+ Settings.UseFileName(nameof(DoubleWideMarkedDayRender));
+
+ var date = new DateTime(2026, 8, 1);
+ // Using an asian-character to have a test covering the unfortunate reality of character rendering of
+ // characters that are visually 1 character in width, but render as double width or some other cursed form.
+ // Written language was a mistake but trying to render it on computers alongside a different one
+ // is arguably worse.
+ var calendar = new CalendarGenerator("็ซ");
+ var sb = new StringBuilder();
+ sb.AppendLine("Double-wide characters will display incorrectly but still tested until rendering can be improved")
+ .AppendLine(calendar.Render(date, date.AddDays(15)));
+
+ return Verify(sb, Settings);
+ }
+}
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/StartDayOfWeekTests.cs b/tests/ModuleTests/Calendar/StartDayOfWeekTests.cs
new file mode 100644
index 0000000..c6680c7
--- /dev/null
+++ b/tests/ModuleTests/Calendar/StartDayOfWeekTests.cs
@@ -0,0 +1,35 @@
+๏ปฟusing System.Text;
+using ModuleCore.Calendar;
+
+namespace ModuleTests.Calendar;
+
+public class StartDayOfWeekTests
+{
+ private static readonly VerifySettings Settings;
+
+ static StartDayOfWeekTests()
+ {
+ Settings = new VerifySettings();
+ var testBaseDirectory = Path.Join(TestConstants.SnapshotFolderName, nameof(StartDayOfWeekTests));
+
+ Settings.UseDirectory(testBaseDirectory);
+ Settings.DisableDiff();
+ }
+
+ [Fact]
+ public Task StartOfDayRender()
+ {
+ Settings.UseFileName(nameof(StartOfDayRender));
+
+ var date = new DateTime(2026, 8, 1);
+ var sb = new StringBuilder();
+ foreach (var dayOfWeek in Enum.GetValues())
+ {
+ var calendar = new CalendarGenerator(startOfWeek: dayOfWeek);
+ sb.AppendLine($"Start Day: {dayOfWeek}");
+ sb.AppendLine(calendar.Render(date, date));
+ }
+
+ return Verify(sb, Settings);
+ }
+}
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/TestData/CalendarTestDates.cs b/tests/ModuleTests/Calendar/TestData/CalendarTestDates.cs
new file mode 100644
index 0000000..189b4a4
--- /dev/null
+++ b/tests/ModuleTests/Calendar/TestData/CalendarTestDates.cs
@@ -0,0 +1,14 @@
+๏ปฟnamespace ModuleTests.Calendar.TestData;
+
+public class CalendarTestDates : TestDataEnumerator
+{
+ public CalendarTestDates()
+ {
+ Data =
+ [
+ new DateTime(2026, 06, 01),
+ new DateTime(2026, 07, 01),
+ new DateTime(2026, 08, 01),
+ ];
+ }
+}
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/TestData/CultureCodeTestData.cs b/tests/ModuleTests/Calendar/TestData/CultureCodeTestData.cs
new file mode 100644
index 0000000..9bdf1f0
--- /dev/null
+++ b/tests/ModuleTests/Calendar/TestData/CultureCodeTestData.cs
@@ -0,0 +1,19 @@
+๏ปฟnamespace ModuleTests.Calendar.TestData;
+
+public class CultureCodeTestData : TestDataEnumerator
+{
+ public CultureCodeTestData()
+ {
+ Data =
+ [
+ "da-DK",
+ "en-AU",
+ "cs-CZ",
+ "es-PR",
+ "fr-LU",
+ "nl-NL",
+ // Will render unaligned due to the character encoding, here to catch if I fix this later
+ "te-IN"
+ ];
+ }
+}
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-06-01.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-06-01.verified.txt
new file mode 100644
index 0000000..de75dd0
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-06-01.verified.txt
@@ -0,0 +1,11 @@
+๏ปฟโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ June 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Mon โ Tue โ Wed โ Thu โ Fri โ Sat โ Sun โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ 1 โ 2 โ 3 โ 4 โ 5 โ 6 โ 7 โ
+โ 8 โ 9 โ 10 โ 11 โ 12 โ 13 โ 14 โ
+โ 15 โ 16 โ 17 โ 18 โ 19 โ 20 โ 21 โ
+โ 22 โ 23 โ 24 โ 25 โ 26 โ 27 โ 28 โ
+โ 29 โ 30 โ โ โ โ โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-07-01.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-07-01.verified.txt
new file mode 100644
index 0000000..a74744c
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-07-01.verified.txt
@@ -0,0 +1,11 @@
+๏ปฟโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ July 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Mon โ Tue โ Wed โ Thu โ Fri โ Sat โ Sun โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ โ โ 1 โ 2 โ 3 โ 4 โ 5 โ
+โ 6 โ 7 โ 8 โ 9 โ 10 โ 11 โ 12 โ
+โ 13 โ 14 โ 15 โ 16 โ 17 โ 18 โ 19 โ
+โ 20 โ 21 โ 22 โ 23 โ 24 โ 25 โ 26 โ
+โ 27 โ 28 โ 29 โ 30 โ 31 โ โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-08-01.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-08-01.verified.txt
new file mode 100644
index 0000000..9598b49
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/2026-08-01.verified.txt
@@ -0,0 +1,12 @@
+๏ปฟโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ August 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Mon โ Tue โ Wed โ Thu โ Fri โ Sat โ Sun โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ โ โ โ โ โ 1 โ 2 โ
+โ 3 โ 4 โ 5 โ 6 โ 7 โ 8 โ 9 โ
+โ 10 โ 11 โ 12 โ 13 โ 14 โ 15 โ 16 โ
+โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ
+โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ
+โ 31 โ โ โ โ โ โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/cs-CZ.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/cs-CZ.verified.txt
new file mode 100644
index 0000000..797c58a
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/cs-CZ.verified.txt
@@ -0,0 +1,12 @@
+๏ปฟโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ srpen 2026 โ
+โโโโโโฌโโโโโฌโโโโโฌโโโโโฌโโโโโฌโโโโโฌโโโโโค
+โ po โ รบt โ st โ ฤt โ pรก โ so โ ne โ
+โโโโโโผโโโโโผโโโโโผโโโโโผโโโโโผโโโโโผโโโโโค
+โ โ โ โ โ โ 1 โ 2 โ
+โ 3 โ 4 โ 5 โ 6 โ 7 โ 8 โ 9 โ
+โ 10 โ 11 โ 12 โ 13 โ 14 โ 15 โ 16 โ
+โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ
+โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ
+โ 31 โ โ โ โ โ โ โ
+โโโโโโดโโโโโดโโโโโดโโโโโดโโโโโดโโโโโดโโโโโ
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/da-DK.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/da-DK.verified.txt
new file mode 100644
index 0000000..58618a1
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/da-DK.verified.txt
@@ -0,0 +1,12 @@
+๏ปฟโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ august 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ man โ tir โ ons โ tor โ fre โ lรธr โ sรธn โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ โ โ โ โ โ 1 โ 2 โ
+โ 3 โ 4 โ 5 โ 6 โ 7 โ 8 โ 9 โ
+โ 10 โ 11 โ 12 โ 13 โ 14 โ 15 โ 16 โ
+โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ
+โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ
+โ 31 โ โ โ โ โ โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/en-AU.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/en-AU.verified.txt
new file mode 100644
index 0000000..9598b49
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/en-AU.verified.txt
@@ -0,0 +1,12 @@
+๏ปฟโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ August 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Mon โ Tue โ Wed โ Thu โ Fri โ Sat โ Sun โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ โ โ โ โ โ 1 โ 2 โ
+โ 3 โ 4 โ 5 โ 6 โ 7 โ 8 โ 9 โ
+โ 10 โ 11 โ 12 โ 13 โ 14 โ 15 โ 16 โ
+โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ
+โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ
+โ 31 โ โ โ โ โ โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/es-PR.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/es-PR.verified.txt
new file mode 100644
index 0000000..9395157
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/es-PR.verified.txt
@@ -0,0 +1,12 @@
+๏ปฟโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ agosto 2026 โ
+โโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโค
+โ lun. โ mar. โ miรฉ. โ jue. โ vie. โ sรกb. โ dom. โ
+โโโโโโโโผโโโโโโโผโโโโโโโผโโโโโโโผโโโโโโโผโโโโโโโผโโโโโโโค
+โ โ โ โ โ โ 1 โ 2 โ
+โ 3 โ 4 โ 5 โ 6 โ 7 โ 8 โ 9 โ
+โ 10 โ 11 โ 12 โ 13 โ 14 โ 15 โ 16 โ
+โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ
+โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ
+โ 31 โ โ โ โ โ โ โ
+โโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโ
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/fr-LU.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/fr-LU.verified.txt
new file mode 100644
index 0000000..101ef77
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/fr-LU.verified.txt
@@ -0,0 +1,12 @@
+๏ปฟโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ aoรปt 2026 โ
+โโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโค
+โ lun. โ mar. โ mer. โ jeu. โ ven. โ sam. โ dim. โ
+โโโโโโโโผโโโโโโโผโโโโโโโผโโโโโโโผโโโโโโโผโโโโโโโผโโโโโโโค
+โ โ โ โ โ โ 1 โ 2 โ
+โ 3 โ 4 โ 5 โ 6 โ 7 โ 8 โ 9 โ
+โ 10 โ 11 โ 12 โ 13 โ 14 โ 15 โ 16 โ
+โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ
+โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ
+โ 31 โ โ โ โ โ โ โ
+โโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโ
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/nl-NL.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/nl-NL.verified.txt
new file mode 100644
index 0000000..1aac604
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/nl-NL.verified.txt
@@ -0,0 +1,12 @@
+๏ปฟโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ augustus 2026 โ
+โโโโโโฌโโโโโฌโโโโโฌโโโโโฌโโโโโฌโโโโโฌโโโโโค
+โ ma โ di โ wo โ do โ vr โ za โ zo โ
+โโโโโโผโโโโโผโโโโโผโโโโโผโโโโโผโโโโโผโโโโโค
+โ โ โ โ โ โ 1 โ 2 โ
+โ 3 โ 4 โ 5 โ 6 โ 7 โ 8 โ 9 โ
+โ 10 โ 11 โ 12 โ 13 โ 14 โ 15 โ 16 โ
+โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ
+โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ
+โ 31 โ โ โ โ โ โ โ
+โโโโโโดโโโโโดโโโโโดโโโโโดโโโโโดโโโโโดโโโโโ
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/te-IN.verified.txt b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/te-IN.verified.txt
new file mode 100644
index 0000000..5568a20
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/BasicRenderTest/te-IN.verified.txt
@@ -0,0 +1,12 @@
+๏ปฟโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ เฐเฐเฐธเฑเฐเฑ 2026 โ
+โโโโโโโโโฌโโโโโโโโฌโโโโโโโโฌโโโโโโโโฌโโโโโโโโฌโโโโโโโโฌโโโโโโโโค
+โ เฐธเฑเฐฎ โ เฐฎเฐเฐเฐณ โ เฐฌเฑเฐง โ เฐเฑเฐฐเฑ โ เฐถเฑเฐเฑเฐฐ โ เฐถเฐจเฐฟ โ เฐเฐฆเฐฟ โ
+โโโโโโโโโผโโโโโโโโผโโโโโโโโผโโโโโโโโผโโโโโโโโผโโโโโโโโผโโโโโโโโค
+โ โ โ โ โ โ 1 โ 2 โ
+โ 3 โ 4 โ 5 โ 6 โ 7 โ 8 โ 9 โ
+โ 10 โ 11 โ 12 โ 13 โ 14 โ 15 โ 16 โ
+โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ
+โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ
+โ 31 โ โ โ โ โ โ โ
+โโโโโโโโโดโโโโโโโโดโโโโโโโโดโโโโโโโโดโโโโโโโโดโโโโโโโโดโโโโโโโโ
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/DefaultMarkedDayRender.verified.txt b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/DefaultMarkedDayRender.verified.txt
new file mode 100644
index 0000000..4181cc1
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/DefaultMarkedDayRender.verified.txt
@@ -0,0 +1,12 @@
+๏ปฟโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ August 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Mon โ Tue โ Wed โ Thu โ Fri โ Sat โ Sun โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ โ โ โ โ โ x โ x โ
+โ x โ x โ x โ x โ x โ x โ x โ
+โ x โ x โ x โ x โ x โ x โ 16 โ
+โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ
+โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ
+โ 31 โ โ โ โ โ โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/DoubleWideMarkedDayRender.verified.txt b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/DoubleWideMarkedDayRender.verified.txt
new file mode 100644
index 0000000..cc27f4f
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/DoubleWideMarkedDayRender.verified.txt
@@ -0,0 +1,13 @@
+๏ปฟDouble-wide characters will display incorrectly but still tested until rendering can be improved
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ August 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Mon โ Tue โ Wed โ Thu โ Fri โ Sat โ Sun โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ โ โ โ โ โ ็ซ โ ็ซ โ
+โ ็ซ โ ็ซ โ ็ซ โ ็ซ โ ็ซ โ ็ซ โ ็ซ โ
+โ ็ซ โ ็ซ โ ็ซ โ ็ซ โ ็ซ โ ็ซ โ 16 โ
+โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ
+โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ
+โ 31 โ โ โ โ โ โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
diff --git a/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/EmojiMarkedDayRender.verified.txt b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/EmojiMarkedDayRender.verified.txt
new file mode 100644
index 0000000..a02a725
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/EmojiMarkedDayRender.verified.txt
@@ -0,0 +1,13 @@
+๏ปฟEmojis will display incorrectly in text but output correctly in terminals with UTF-8 encoding.
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ August 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Mon โ Tue โ Wed โ Thu โ Fri โ Sat โ Sun โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ โ โ โ โ โ ๐คซ โ ๐คซ โ
+โ ๐คซ โ ๐คซ โ ๐คซ โ ๐คซ โ ๐คซ โ ๐คซ โ ๐คซ โ
+โ ๐คซ โ ๐คซ โ ๐คซ โ ๐คซ โ ๐คซ โ ๐คซ โ 16 โ
+โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ
+โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ
+โ 31 โ โ โ โ โ โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
diff --git a/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/LongMarkedDayRender.verified.txt b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/LongMarkedDayRender.verified.txt
new file mode 100644
index 0000000..a8ebbf6
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/LongMarkedDayRender.verified.txt
@@ -0,0 +1,12 @@
+๏ปฟโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ August 2026 โ
+โโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโค
+โ Mon โ Tue โ Wed โ Thu โ Fri โ Sat โ Sun โ
+โโโโโโโโโโผโโโโโโโโโผโโโโโโโโโผโโโโโโโโโผโโโโโโโโโผโโโโโโโโโผโโโโโโโโโค
+โ โ โ โ โ โ passed โ passed โ
+โ passed โ passed โ passed โ passed โ passed โ passed โ passed โ
+โ passed โ passed โ passed โ passed โ passed โ passed โ 16 โ
+โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ
+โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ
+โ 31 โ โ โ โ โ โ โ
+โโโโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโโ
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/SimpleMarkedDayRender.verified.txt b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/SimpleMarkedDayRender.verified.txt
new file mode 100644
index 0000000..f546aac
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/MarkedDayRenderTests/SimpleMarkedDayRender.verified.txt
@@ -0,0 +1,12 @@
+๏ปฟโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ August 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Mon โ Tue โ Wed โ Thu โ Fri โ Sat โ Sun โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ โ โ โ โ โ | โ | โ
+โ | โ | โ | โ | โ | โ | โ | โ
+โ | โ | โ | โ | โ | โ | โ 16 โ
+โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ
+โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ
+โ 31 โ โ โ โ โ โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
\ No newline at end of file
diff --git a/tests/ModuleTests/Calendar/snapshots/StartDayOfWeekTests/StartOfDayRender.verified.txt b/tests/ModuleTests/Calendar/snapshots/StartDayOfWeekTests/StartOfDayRender.verified.txt
new file mode 100644
index 0000000..8393453
--- /dev/null
+++ b/tests/ModuleTests/Calendar/snapshots/StartDayOfWeekTests/StartOfDayRender.verified.txt
@@ -0,0 +1,86 @@
+๏ปฟStart Day: Sunday
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ August 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Sun โ Mon โ Tue โ Wed โ Thu โ Fri โ Sat โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ โ โ โ โ โ โ 1 โ
+โ 2 โ 3 โ 4 โ 5 โ 6 โ 7 โ 8 โ
+โ 9 โ 10 โ 11 โ 12 โ 13 โ 14 โ 15 โ
+โ 16 โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ
+โ 23 โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ
+โ 30 โ 31 โ โ โ โ โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
+Start Day: Monday
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ August 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Mon โ Tue โ Wed โ Thu โ Fri โ Sat โ Sun โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ โ โ โ โ โ 1 โ 2 โ
+โ 3 โ 4 โ 5 โ 6 โ 7 โ 8 โ 9 โ
+โ 10 โ 11 โ 12 โ 13 โ 14 โ 15 โ 16 โ
+โ 17 โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ
+โ 24 โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ
+โ 31 โ โ โ โ โ โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
+Start Day: Tuesday
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ August 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Tue โ Wed โ Thu โ Fri โ Sat โ Sun โ Mon โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ โ โ โ โ 1 โ 2 โ 3 โ
+โ 4 โ 5 โ 6 โ 7 โ 8 โ 9 โ 10 โ
+โ 11 โ 12 โ 13 โ 14 โ 15 โ 16 โ 17 โ
+โ 18 โ 19 โ 20 โ 21 โ 22 โ 23 โ 24 โ
+โ 25 โ 26 โ 27 โ 28 โ 29 โ 30 โ 31 โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
+Start Day: Wednesday
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ August 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Wed โ Thu โ Fri โ Sat โ Sun โ Mon โ Tue โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ โ โ โ 1 โ 2 โ 3 โ 4 โ
+โ 5 โ 6 โ 7 โ 8 โ 9 โ 10 โ 11 โ
+โ 12 โ 13 โ 14 โ 15 โ 16 โ 17 โ 18 โ
+โ 19 โ 20 โ 21 โ 22 โ 23 โ 24 โ 25 โ
+โ 26 โ 27 โ 28 โ 29 โ 30 โ 31 โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
+Start Day: Thursday
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ August 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Thu โ Fri โ Sat โ Sun โ Mon โ Tue โ Wed โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ โ โ 1 โ 2 โ 3 โ 4 โ 5 โ
+โ 6 โ 7 โ 8 โ 9 โ 10 โ 11 โ 12 โ
+โ 13 โ 14 โ 15 โ 16 โ 17 โ 18 โ 19 โ
+โ 20 โ 21 โ 22 โ 23 โ 24 โ 25 โ 26 โ
+โ 27 โ 28 โ 29 โ 30 โ 31 โ โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
+Start Day: Friday
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ August 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Fri โ Sat โ Sun โ Mon โ Tue โ Wed โ Thu โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ โ 1 โ 2 โ 3 โ 4 โ 5 โ 6 โ
+โ 7 โ 8 โ 9 โ 10 โ 11 โ 12 โ 13 โ
+โ 14 โ 15 โ 16 โ 17 โ 18 โ 19 โ 20 โ
+โ 21 โ 22 โ 23 โ 24 โ 25 โ 26 โ 27 โ
+โ 28 โ 29 โ 30 โ 31 โ โ โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
+Start Day: Saturday
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ August 2026 โ
+โโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโฌโโโโโโค
+โ Sat โ Sun โ Mon โ Tue โ Wed โ Thu โ Fri โ
+โโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโผโโโโโโค
+โ 1 โ 2 โ 3 โ 4 โ 5 โ 6 โ 7 โ
+โ 8 โ 9 โ 10 โ 11 โ 12 โ 13 โ 14 โ
+โ 15 โ 16 โ 17 โ 18 โ 19 โ 20 โ 21 โ
+โ 22 โ 23 โ 24 โ 25 โ 26 โ 27 โ 28 โ
+โ 29 โ 30 โ 31 โ โ โ โ โ
+โโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโดโโโโโโ
diff --git a/tests/ModuleTests/ModuleTests.csproj b/tests/ModuleTests/ModuleTests.csproj
new file mode 100644
index 0000000..7343cfd
--- /dev/null
+++ b/tests/ModuleTests/ModuleTests.csproj
@@ -0,0 +1,25 @@
+๏ปฟ
+
+
+ net10.0
+ enable
+ enable
+ false
+ Exe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/ModuleTests/TestConstants.cs b/tests/ModuleTests/TestConstants.cs
new file mode 100644
index 0000000..e32b3cd
--- /dev/null
+++ b/tests/ModuleTests/TestConstants.cs
@@ -0,0 +1,6 @@
+๏ปฟnamespace ModuleTests;
+
+public class TestConstants
+{
+ public const string SnapshotFolderName = "Snapshots";
+}
\ No newline at end of file
diff --git a/tests/ModuleTests/TestDataEnumerator.cs b/tests/ModuleTests/TestDataEnumerator.cs
new file mode 100644
index 0000000..92937f5
--- /dev/null
+++ b/tests/ModuleTests/TestDataEnumerator.cs
@@ -0,0 +1,15 @@
+๏ปฟusing System.Collections;
+
+namespace ModuleTests;
+
+public abstract class TestDataEnumerator : IEnumerable>
+{
+ protected IEnumerable Data { get; init; } = [];
+
+ public IEnumerator> GetEnumerator()
+ {
+ return Data.Select(data => new TheoryDataRow(data)).GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+}
\ No newline at end of file