Updated the BumperPlaceholders class to streamline token extraction from texts, enhancing performance and clarity. Modified BumperFacts to initialize slot titles with an empty array instead of a dictionary for better consistency. Renamed methods in BumperResolver for clarity, and refactored GridScheduleGenerator to simplify return logic. Additionally, improved the BumperLinesEditor component by implementing a keyed list for better state management and user experience.
208 lines
8.1 KiB
C#
208 lines
8.1 KiB
C#
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using TeleWave.Application.Broadcast.Bumpers;
|
|
using TeleWave.Application.Common.Interfaces;
|
|
using TeleWave.Application.Streaming;
|
|
using TeleWave.Application.Tests.Support;
|
|
using TeleWave.Domain.Broadcast;
|
|
using TeleWave.Domain.Images;
|
|
using TeleWave.Domain.Library;
|
|
using TeleWave.Domain.Programming;
|
|
using TeleWave.Infrastructure.Persistence;
|
|
using Xunit;
|
|
|
|
namespace TeleWave.Application.Tests.Broadcast;
|
|
|
|
/// <summary>
|
|
/// Превью ТВ-заставки: рендерится каждый подблок блока, длительность выравнивается по длине
|
|
/// сегмента HLS, а плейсхолдеры подставляются образцами выбранного канала — блок общий, но
|
|
/// смотреть на него надо глазами конкретного канала.
|
|
/// </summary>
|
|
public class BumperPreviewTests
|
|
{
|
|
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
|
|
|
private static RenderBumperPreviewCommandHandler Handler(
|
|
AppDbContext db,
|
|
IBumperRenderer? renderer = null,
|
|
IBumperTemplateStorage? storage = null,
|
|
IImageStore? imageStore = null
|
|
) =>
|
|
new(
|
|
db,
|
|
renderer ?? Substitute.For<IBumperRenderer>(),
|
|
storage ?? Substitute.For<IBumperTemplateStorage>(),
|
|
imageStore ?? Substitute.For<IImageStore>(),
|
|
Options.Create(new BumperOptions()),
|
|
Options.Create(new StreamingOptions { SegmentSeconds = 5 })
|
|
);
|
|
|
|
private static BumperTemplate NewTemplate()
|
|
{
|
|
var template = BumperTemplate.Create("Блок", "Текст 1");
|
|
template
|
|
.Variants[0]
|
|
.SetLines([
|
|
BumperLine.Create(0, BumperLineStyle.Label, BumperLineColor.Accent, "СЕЙЧАС"),
|
|
BumperLine.Create(1, BumperLineStyle.Title, BumperLineColor.Text, "{now.title}"),
|
|
BumperLine.Create(2, BumperLineStyle.Label, BumperLineColor.Accent, "ДАЛЕЕ"),
|
|
BumperLine.Create(3, BumperLineStyle.Title, BumperLineColor.Text, "{next.title}"),
|
|
]);
|
|
return template;
|
|
}
|
|
|
|
private static IReadOnlyList<BumperRenderSpec> Specs(IBumperRenderer renderer) =>
|
|
renderer
|
|
.ReceivedCalls()
|
|
.Select(c => c.GetArguments()[1])
|
|
.OfType<BumperRenderSpec>()
|
|
.ToList();
|
|
|
|
[Fact]
|
|
public async Task Render_UnknownTemplate_ReturnsNotFound()
|
|
{
|
|
var fixture = new TestDb();
|
|
await using var db = fixture.New();
|
|
|
|
var result = await Handler(db)
|
|
.Handle(new RenderBumperPreviewCommand(Guid.NewGuid(), null), CancellationToken.None);
|
|
|
|
Assert.Equal(BumperErrors.TemplateNotFound, result.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Render_DrawsEveryVariant_WithChannelSamplesAndAlignedDuration()
|
|
{
|
|
var fixture = new TestDb();
|
|
var channel = Channel.Create("Первый", "one", T0);
|
|
var template = NewTemplate();
|
|
var second = template.AddVariant("Второй подблок");
|
|
second.SetLines([
|
|
BumperLine.Create(0, BumperLineStyle.Title, BumperLineColor.Text, "{channel}"),
|
|
]);
|
|
|
|
// Шоу канала: слот сетки ссылается на группу, в группе — шоу.
|
|
var show = Show.Create("Наше шоу", ShowKind.Series);
|
|
var group = Group.Create("Кино");
|
|
group.AddElement(GroupElementKind.Show, show.Id);
|
|
var grid = ScheduleTemplate.Create(channel.Id, "Сетка");
|
|
var slot = grid.Background!.AddSlot("Круглосуточно", new TimeOnly(6, 0), 24 * 60);
|
|
slot.UpdateContent(
|
|
new SlotContent(
|
|
slot.Title,
|
|
SlotKind.Content,
|
|
group.Id,
|
|
null,
|
|
null,
|
|
SlotBlockMode.FillSlot,
|
|
1,
|
|
OverflowPolicy.ContinueNext
|
|
)
|
|
);
|
|
channel.SetTemplate(grid.Id);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.Channels.Add(channel);
|
|
seed.BumperTemplates.Add(template);
|
|
seed.Shows.Add(show);
|
|
seed.Groups.Add(group);
|
|
seed.ScheduleTemplates.Add(grid);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var renderer = Substitute.For<IBumperRenderer>();
|
|
await using var db = fixture.New();
|
|
var result = await Handler(db, renderer)
|
|
.Handle(
|
|
new RenderBumperPreviewCommand(template.Id, channel.Id),
|
|
CancellationToken.None
|
|
);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
// Оба подблока отрисованы, каждый — в свой ассет-превью.
|
|
await renderer
|
|
.Received(1)
|
|
.RenderAsync(
|
|
BumperPreview.AssetId(template.Variants[0].Id),
|
|
Arg.Any<BumperRenderSpec>(),
|
|
Arg.Any<CancellationToken>()
|
|
);
|
|
await renderer
|
|
.Received(1)
|
|
.RenderAsync(
|
|
BumperPreview.AssetId(second.Id),
|
|
Arg.Any<BumperRenderSpec>(),
|
|
Arg.Any<CancellationToken>()
|
|
);
|
|
|
|
var specs = Specs(renderer);
|
|
// Своего звука у блока нет: 8 секунд по умолчанию, выровненные вверх до сегмента в 5 сек.
|
|
Assert.All(specs, s => Assert.Equal(10, s.DurationSeconds));
|
|
|
|
var nowNext = specs.First(s => s.Lines.Count == 4);
|
|
Assert.Equal("Наше шоу", nowNext.Lines[1].Text);
|
|
|
|
var channelLine = specs.First(s => s.Lines.Count == 1);
|
|
Assert.Equal("Первый", channelLine.Lines[0].Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Render_WithBackgroundImage_ResolvesPathFromRegistry()
|
|
{
|
|
var fixture = new TestDb();
|
|
var template = NewTemplate();
|
|
var image = Image.Create(ImageCategory.BumperBackground, ".png", "bg.png");
|
|
template.SetBackgroundImage(image.Id);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.BumperTemplates.Add(template);
|
|
seed.Images.Add(image);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var renderer = Substitute.For<IBumperRenderer>();
|
|
var imageStore = Substitute.For<IImageStore>();
|
|
imageStore.ResolvePath(image.Id, ".png").Returns("/data/images/bg.png");
|
|
|
|
await using var db = fixture.New();
|
|
var result = await Handler(db, renderer, imageStore: imageStore)
|
|
.Handle(new RenderBumperPreviewCommand(template.Id, null), CancellationToken.None);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
var spec = Specs(renderer)[0];
|
|
Assert.Equal("/data/images/bg.png", spec.BackgroundFile);
|
|
// Канала нет — образцы заглушечные, но кадр всё равно собирается.
|
|
Assert.Equal("Первое шоу", spec.Lines[1].Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Render_DropsLinesThatResolveToNothing()
|
|
{
|
|
var fixture = new TestDb();
|
|
var template = BumperTemplate.Create("Блок", "Текст 1");
|
|
template
|
|
.Variants[0]
|
|
.SetLines([
|
|
BumperLine.Create(0, BumperLineStyle.Label, BumperLineColor.Accent, "{next.genre}"),
|
|
BumperLine.Create(1, BumperLineStyle.Title, BumperLineColor.Text, "{next.title}"),
|
|
]);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.BumperTemplates.Add(template);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var renderer = Substitute.For<IBumperRenderer>();
|
|
await using var db = fixture.New();
|
|
await Handler(db, renderer)
|
|
.Handle(new RenderBumperPreviewCommand(template.Id, null), CancellationToken.None);
|
|
|
|
// Жанра у образца нет — строка схлопнулась, а не оставила дыру в кадре.
|
|
var spec = Specs(renderer)[0];
|
|
Assert.Single(spec.Lines);
|
|
}
|
|
}
|