feat(text): persist independent text layer settings

Add additive defaults and round-trip tests for per-layer text content and formatting. Model the static text source after OBS text-source conventions: https://obsproject.com/kb/text-sources
This commit is contained in:
2026-11-04 14:22:32 -08:00
parent 106896c408
commit 60c4a0032f
8 changed files with 283 additions and 5 deletions
@@ -15,6 +15,97 @@ namespace ytLive.Tests;
/// </summary>
public class LayoutStorePersistenceTests
{
[Fact]
public void Text_Layers_RoundTrip_Independently_And_Legacy_Rows_Get_Defaults()
{
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
try
{
var scene = new Scene { Name = "Starting" };
scene.Elements.Add(new Source
{
Name = "Headline",
Type = SourceType.TextOverlay,
TextContent = "First line\nSecond line",
TextFontFamily = "Arial",
TextFontSize = 72,
TextColor = "#12AB34",
TextBold = true,
TextAlignment = TextLayerAlignment.Center,
});
scene.Elements.Add(new Source
{
Name = "Caption",
Type = SourceType.TextOverlay,
TextContent = "Independent caption",
TextFontFamily = "Segoe UI",
TextFontSize = 24,
TextColor = "#80445566",
TextItalic = true,
TextUnderline = true,
TextAlignment = TextLayerAlignment.Right,
});
using (var store = new LayoutStore(path))
store.Save(new[] { scene }, null, null);
using (var reopened = new LayoutStore(path))
{
var sources = Assert.Single(reopened.Load()).Elements.OfType<Source>()
.Where(source => source.Type == SourceType.TextOverlay).ToArray();
Assert.Equal(2, sources.Length);
Assert.Equal("First line\nSecond line", sources[0].TextContent);
Assert.Equal("Arial", sources[0].TextFontFamily);
Assert.Equal(72, sources[0].TextFontSize);
Assert.Equal("#12AB34", sources[0].TextColor);
Assert.True(sources[0].TextBold);
Assert.False(sources[0].TextItalic);
Assert.False(sources[0].TextUnderline);
Assert.Equal(TextLayerAlignment.Center, sources[0].TextAlignment);
Assert.Equal("Independent caption", sources[1].TextContent);
Assert.Equal(24, sources[1].TextFontSize);
Assert.Equal("#80445566", sources[1].TextColor);
Assert.False(sources[1].TextBold);
Assert.True(sources[1].TextItalic);
Assert.True(sources[1].TextUnderline);
Assert.Equal(TextLayerAlignment.Right, sources[1].TextAlignment);
}
SqliteConnection.ClearAllPools();
using (var raw = new SqliteConnection($"Data Source={path}"))
{
raw.Open();
using var cmd = raw.CreateCommand();
cmd.CommandText = "ALTER TABLE Source DROP COLUMN TextContent; ALTER TABLE Source DROP COLUMN TextFontFamily; ALTER TABLE Source DROP COLUMN TextFontSize; ALTER TABLE Source DROP COLUMN TextColor; ALTER TABLE Source DROP COLUMN TextBold; ALTER TABLE Source DROP COLUMN TextItalic; ALTER TABLE Source DROP COLUMN TextUnderline; ALTER TABLE Source DROP COLUMN TextAlignment;";
cmd.ExecuteNonQuery();
}
SqliteConnection.ClearAllPools();
using (var legacy = new LayoutStore(path))
{
var sources = Assert.Single(legacy.Load()).Elements.OfType<Source>()
.Where(source => source.Type == SourceType.TextOverlay).ToArray();
Assert.Equal(2, sources.Length);
foreach (var source in sources)
{
Assert.Equal("Text", source.TextContent);
Assert.Equal("Segoe UI", source.TextFontFamily);
Assert.Equal(48, source.TextFontSize);
Assert.Equal("#FFFFFF", source.TextColor);
Assert.False(source.TextBold);
Assert.False(source.TextItalic);
Assert.False(source.TextUnderline);
Assert.Equal(TextLayerAlignment.Left, source.TextAlignment);
}
}
}
finally
{
SqliteConnection.ClearAllPools();
try { File.Delete(path); } catch { /* best-effort cleanup */ }
}
}
/// <summary>TASK 47: the alert announcement display method must survive a
/// restart. This one is easy to get quietly wrong — the Source row is read back
/// by POSITIONAL index, so a column inserted anywhere but the end silently