Files
TeleWave/backend/tests/TeleWave.Application.Tests/Broadcast/BumperPreviewTests.cs
T
Leonid Pershin 1afe01aed8
ci / build-backend (push) Successful in 2m15s
ci / build-frontend (push) Successful in 1m7s
ci / tests (push) Successful in 2m33s
ci / sonar (push) Successful in 10m26s
Refactor BumperPlaceholders and BumperFacts for improved logic and readability
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.
2026-07-27 23:04:15 +03:00

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);
}
}