238 lines
9.5 KiB
C#
238 lines
9.5 KiB
C#
using System;
|
|
using System.IO;
|
|
using Microsoft.Data.Sqlite;
|
|
using Xunit;
|
|
using ytLive.Models;
|
|
using ytLive.Services;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// The layout DB is a full rewrite on every save (DELETE all scenes/sources,
|
|
/// re-insert from memory). This guards the round trip: webcam configs the user
|
|
/// removes in the UI must not come back after a save + reload.
|
|
/// </summary>
|
|
public class LayoutStorePersistenceTests
|
|
{
|
|
[Fact]
|
|
public void Deleted_Webcam_Config_Does_Not_Return_After_Save_And_Reload()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
|
|
try
|
|
{
|
|
using var store = new LayoutStore(path);
|
|
var webcam = new Webcam { DeviceId = "real-device", Name = "Logitech" };
|
|
var scene = new Scene { Name = "Starting" };
|
|
scene.Elements.Add(new WebcamSceneConfig
|
|
{
|
|
WebcamId = webcam.Id,
|
|
Name = webcam.Name,
|
|
Width = 480,
|
|
Height = 270,
|
|
});
|
|
store.Save(new[] { scene }, webcam, null);
|
|
|
|
var reloaded = store.Load();
|
|
var config = Assert.Single(reloaded[0].Elements);
|
|
Assert.IsType<WebcamSceneConfig>(config);
|
|
|
|
reloaded[0].Elements.RemoveAt(0);
|
|
store.Save(reloaded, store.Webcam, null);
|
|
|
|
var afterDelete = store.Load();
|
|
Assert.Empty(afterDelete[0].Elements);
|
|
}
|
|
finally
|
|
{
|
|
SqliteConnection.ClearAllPools();
|
|
try { File.Delete(path); } catch { /* best-effort cleanup */ }
|
|
}
|
|
}
|
|
|
|
// The backdrop (schema v5): permanent bottom layer holding live desktop/game
|
|
// capture. IsBackdrop + CaptureKey must survive a save + reload so a
|
|
// re-designated target isn't lost on restart.
|
|
[Fact]
|
|
public void Backdrop_IsBackdrop_And_CaptureKey_Survive_Save_And_Reload()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
|
|
try
|
|
{
|
|
using var store = new LayoutStore(path);
|
|
var backdrop = new Source
|
|
{
|
|
Name = "Backdrop",
|
|
Type = SourceType.DisplayCapture,
|
|
IsBackdrop = true,
|
|
IsEnabled = true,
|
|
X = 0,
|
|
Y = 0,
|
|
Width = 1920,
|
|
Height = 1080,
|
|
MonitorIndex = 1,
|
|
CaptureKey = "monitor:1",
|
|
};
|
|
var scene = new Scene { Name = "Starting" };
|
|
scene.Elements.Add(backdrop);
|
|
store.Save(new[] { scene }, null, null);
|
|
|
|
var reloaded = store.Load();
|
|
var restored = Assert.IsType<Source>(Assert.Single(reloaded[0].Elements));
|
|
|
|
Assert.True(restored.IsBackdrop);
|
|
Assert.Equal("monitor:1", restored.CaptureKey);
|
|
Assert.Equal(1, restored.MonitorIndex);
|
|
Assert.Equal(SourceType.DisplayCapture, restored.Type);
|
|
}
|
|
finally
|
|
{
|
|
SqliteConnection.ClearAllPools();
|
|
try { File.Delete(path); } catch { /* best-effort cleanup */ }
|
|
}
|
|
}
|
|
|
|
// Per-scene backdrop switch (schema v6): a scene whose backdrop was turned
|
|
// off must stay off across a save + reload.
|
|
[Fact]
|
|
public void Scene_HasBackdrop_Survives_Save_And_Reload()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
|
|
try
|
|
{
|
|
using var store = new LayoutStore(path);
|
|
var on = new Scene { Name = "Live", HasBackdrop = true };
|
|
var off = new Scene { Name = "Ending", HasBackdrop = false };
|
|
store.Save(new[] { on, off }, null, null);
|
|
|
|
var reloaded = store.Load();
|
|
|
|
Assert.True(reloaded.First(s => s.Name == "Live").HasBackdrop);
|
|
Assert.False(reloaded.First(s => s.Name == "Ending").HasBackdrop);
|
|
}
|
|
finally
|
|
{
|
|
SqliteConnection.ClearAllPools();
|
|
try { File.Delete(path); } catch { /* best-effort cleanup */ }
|
|
}
|
|
}
|
|
|
|
// v5 → v6 one-time backfill: pre-v6 DBs already have backdrops sitting in the
|
|
// non-Live canonical scenes (Starting/BRB/Chat/Ending). Adding the HasBackdrop
|
|
// column turns those scenes off and drops their backdrop sources; Live keeps
|
|
// its backdrop.
|
|
[Fact]
|
|
public void V5_Database_Backfill_Removes_NonLive_Backdrops()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
|
|
try
|
|
{
|
|
using (var conn = new SqliteConnection($"Data Source={path}"))
|
|
{
|
|
conn.Open();
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = """
|
|
CREATE TABLE Scene (
|
|
Id TEXT PRIMARY KEY,
|
|
Name TEXT NOT NULL,
|
|
IsHidden INTEGER NOT NULL DEFAULT 0,
|
|
IsChatScene INTEGER NOT NULL DEFAULT 0,
|
|
SortOrder INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
CREATE TABLE Source (
|
|
Id TEXT PRIMARY KEY,
|
|
SceneId TEXT NOT NULL REFERENCES Scene(Id) ON DELETE CASCADE,
|
|
AssetId TEXT,
|
|
Type TEXT NOT NULL,
|
|
Name TEXT NOT NULL,
|
|
IsEnabled INTEGER NOT NULL DEFAULT 1,
|
|
X REAL NOT NULL DEFAULT 0,
|
|
Y REAL NOT NULL DEFAULT 0,
|
|
Width REAL NOT NULL DEFAULT 0,
|
|
Height REAL NOT NULL DEFAULT 0,
|
|
Opacity REAL NOT NULL DEFAULT 1,
|
|
MonitorIndex INTEGER,
|
|
DeviceId TEXT,
|
|
ClipShape TEXT NOT NULL DEFAULT 'Traditional',
|
|
IsMirrored INTEGER NOT NULL DEFAULT 0,
|
|
IsBackdrop INTEGER NOT NULL DEFAULT 0,
|
|
CaptureKey TEXT,
|
|
SortOrder INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
INSERT INTO Scene (Id, Name, SortOrder) VALUES ('s-starting', 'Starting', 0);
|
|
INSERT INTO Scene (Id, Name, SortOrder) VALUES ('s-live', 'Live', 1);
|
|
INSERT INTO Scene (Id, Name, SortOrder) VALUES ('s-chat', 'Chat', 2);
|
|
INSERT INTO Source (Id, SceneId, Type, Name, IsBackdrop, CaptureKey, SortOrder)
|
|
VALUES ('b-starting', 's-starting', 'DisplayCapture', 'Backdrop', 1, 'monitor:0', 0);
|
|
INSERT INTO Source (Id, SceneId, Type, Name, IsBackdrop, CaptureKey, SortOrder)
|
|
VALUES ('b-live', 's-live', 'DisplayCapture', 'Backdrop', 1, 'monitor:0', 0);
|
|
INSERT INTO Source (Id, SceneId, Type, Name, IsBackdrop, CaptureKey, SortOrder)
|
|
VALUES ('b-chat', 's-chat', 'DisplayCapture', 'Backdrop', 1, 'monitor:0', 0);
|
|
PRAGMA user_version = 5;
|
|
""";
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
using var store = new LayoutStore(path);
|
|
var scenes = store.Load();
|
|
|
|
var starting = Assert.Single(scenes, s => s.Name == "Starting");
|
|
Assert.False(starting.HasBackdrop);
|
|
Assert.DoesNotContain(starting.Elements, e => e is Source { IsBackdrop: true });
|
|
|
|
var chat = Assert.Single(scenes, s => s.Name == "Chat");
|
|
Assert.False(chat.HasBackdrop);
|
|
Assert.DoesNotContain(chat.Elements, e => e is Source { IsBackdrop: true });
|
|
|
|
var live = Assert.Single(scenes, s => s.Name == "Live");
|
|
Assert.True(live.HasBackdrop);
|
|
Assert.Single(live.Elements, e => e is Source { IsBackdrop: true });
|
|
}
|
|
finally
|
|
{
|
|
SqliteConnection.ClearAllPools();
|
|
try { File.Delete(path); } catch { /* best-effort cleanup */ }
|
|
}
|
|
}
|
|
|
|
// Round-to-rect restore is persisted (schema v4): a Round webcam resized to a
|
|
// square saves its pre-Round rect dims, and a reloaded config restores them on
|
|
// toggle-back instead of staying square.
|
|
[Fact]
|
|
public void Pre_Round_Rect_Dims_Survive_Save_And_Reload()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
|
|
try
|
|
{
|
|
using var store = new LayoutStore(path);
|
|
var webcam = new Webcam { DeviceId = "real-device", Name = "Logitech" };
|
|
var scene = new Scene { Name = "Starting" };
|
|
scene.Elements.Add(new WebcamSceneConfig
|
|
{
|
|
WebcamId = webcam.Id,
|
|
Name = webcam.Name,
|
|
Width = 400,
|
|
Height = 400,
|
|
ClipShape = ClipShape.Round,
|
|
RectWidth = 480,
|
|
RectHeight = 270,
|
|
});
|
|
store.Save(new[] { scene }, webcam, null);
|
|
|
|
var reloaded = store.Load();
|
|
var config = Assert.IsType<WebcamSceneConfig>(Assert.Single(reloaded[0].Elements));
|
|
|
|
config.ToggleClipShape();
|
|
Assert.Equal(ClipShape.Traditional, config.ClipShape);
|
|
Assert.Equal(480, config.Width);
|
|
Assert.Equal(270, config.Height);
|
|
Assert.Null(config.RectWidth);
|
|
Assert.Null(config.RectHeight);
|
|
}
|
|
finally
|
|
{
|
|
SqliteConnection.ClearAllPools();
|
|
try { File.Delete(path); } catch { /* best-effort cleanup */ }
|
|
}
|
|
}
|
|
}
|