Refactored the GridPlanner class to optimize slot management during grid generation, ensuring more efficient handling of slots. Updated the GenerateGridDialog component to improve its layout and usability, enhancing the overall user experience when interacting with slot lists. These changes contribute to better functionality and accessibility in grid management.
234 lines
8.7 KiB
C#
234 lines
8.7 KiB
C#
using TeleWave.Application.Programming.Groups;
|
|
using TeleWave.Application.Programming.Templates;
|
|
using TeleWave.Application.Programming.Templates.Generate;
|
|
using TeleWave.Application.Tests.Support;
|
|
using TeleWave.Domain.Broadcast;
|
|
using TeleWave.Domain.Library;
|
|
using TeleWave.Domain.Media;
|
|
using TeleWave.Domain.Programming;
|
|
using Xunit;
|
|
|
|
namespace TeleWave.Application.Tests.Programming;
|
|
|
|
/// <summary>
|
|
/// Витрина автосборки: каталог профилей, предпросмотр плана и его перевод в DTO. Предпросмотр —
|
|
/// то, по чему админ принимает решение, поэтому проверяется отдельно от самой раскладки.
|
|
/// </summary>
|
|
public class GridPlanPreviewTests
|
|
{
|
|
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
|
|
|
private static PlannedSlot Slot(
|
|
string title,
|
|
SlotKind kind,
|
|
SlotBlockMode blockMode,
|
|
int blockValue,
|
|
int? weekday = null
|
|
) =>
|
|
new(
|
|
GridPlanLayer.Main,
|
|
weekday,
|
|
new TimeOnly(20, 0),
|
|
120,
|
|
title,
|
|
Daypart.Prime,
|
|
kind,
|
|
kind == SlotKind.Content ? Guid.NewGuid() : null,
|
|
kind == SlotKind.Content ? title : null,
|
|
blockMode,
|
|
blockValue,
|
|
null,
|
|
null,
|
|
IsAnchor: false
|
|
);
|
|
|
|
[Fact]
|
|
public async Task Profiles_AreListed_WithReferenceAndDescription()
|
|
{
|
|
var result = await new ListGridProfilesQueryHandler().Handle(
|
|
new ListGridProfilesQuery(),
|
|
CancellationToken.None
|
|
);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
// Каждый профиль должен объяснять себя: имя, на что похоже и для какой библиотеки годится.
|
|
Assert.Equal(GridProfiles.All.Count, result.Value.Count);
|
|
Assert.All(
|
|
result.Value,
|
|
profile =>
|
|
{
|
|
Assert.False(string.IsNullOrWhiteSpace(profile.Name));
|
|
Assert.False(string.IsNullOrWhiteSpace(profile.Reference));
|
|
Assert.False(string.IsNullOrWhiteSpace(profile.Description));
|
|
}
|
|
);
|
|
Assert.Contains(result.Value, p => p.Kind == GridProfileKind.Animation);
|
|
}
|
|
|
|
[Fact]
|
|
public void Mapper_DescribesEveryBlockKind()
|
|
{
|
|
var plan = new GridPlan(
|
|
GridProfiles.Get(GridProfileKind.Mixed),
|
|
GridGenerationMode.Rebuild,
|
|
[
|
|
Slot("Сериалы", SlotKind.Content, SlotBlockMode.Count, 3),
|
|
Slot("Кино", SlotKind.Content, SlotBlockMode.FillSlot, 1),
|
|
Slot("Полоса", SlotKind.Content, SlotBlockMode.Duration, 90, weekday: 6),
|
|
Slot("Повтор", SlotKind.Repeat, SlotBlockMode.FillSlot, 1),
|
|
Slot("Конец вещания", SlotKind.SignOff, SlotBlockMode.FillSlot, 1),
|
|
],
|
|
["замечание"],
|
|
SlotsToRemove: 4,
|
|
FallbackGroupId: Guid.NewGuid(),
|
|
FallbackGroupName: "Фон",
|
|
FreeMinutes: 10080,
|
|
CoveredMinutes: 5040
|
|
);
|
|
|
|
var dto = plan.ToDto();
|
|
|
|
Assert.Equal(GridProfileKind.Mixed, dto.Profile);
|
|
Assert.Equal(plan.Profile.Name, dto.ProfileName);
|
|
Assert.Equal(4, dto.SlotsToRemove);
|
|
Assert.Equal("Фон", dto.FallbackGroupName);
|
|
Assert.Equal(["замечание"], dto.Notes);
|
|
|
|
// Описание блока — то, что читает админ вместо режима и числа.
|
|
Assert.Equal(
|
|
["3 подряд", "до конца слота", "90 мин", "повтор", "конец вещания"],
|
|
dto.Slots.Select(s => s.Block)
|
|
);
|
|
// Время приезжает строкой «ЧЧ:ММ»: в предпросмотре оно только показывается.
|
|
Assert.All(dto.Slots, slot => Assert.Equal("20:00", slot.Start));
|
|
Assert.Equal(6, dto.Slots[2].Weekday);
|
|
Assert.Null(dto.Slots[0].Weekday);
|
|
// У неконтентных слотов группы нет — показывать в колонке нечего.
|
|
Assert.Null(dto.Slots[3].GroupName);
|
|
Assert.Equal("Сериалы", dto.Slots[0].GroupName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Preview_ReturnsPlanForProfile()
|
|
{
|
|
var fixture = new TestDb();
|
|
var channelId = await SeedAsync(fixture);
|
|
|
|
await using var db = fixture.New();
|
|
var result = await new PreviewGeneratedGridQueryHandler(
|
|
new GridPlanner(db, GroupServices.Dynamic(db), new GroupElementResolver(db))
|
|
).Handle(
|
|
new PreviewGeneratedGridQuery(
|
|
channelId,
|
|
GridProfileKind.Kids,
|
|
GridGenerationMode.Rebuild
|
|
),
|
|
CancellationToken.None
|
|
);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Equal(GridProfileKind.Kids, result.Value.Profile);
|
|
Assert.NotEmpty(result.Value.Slots);
|
|
// Пересборка сносит то, что было, и это видно до нажатия.
|
|
Assert.True(result.Value.SlotsToRemove >= 0);
|
|
Assert.Equal(7 * GridCoverage.MinutesInDay, result.Value.FreeMinutes);
|
|
// У детского профиля ночь — конец вещания, и в предпросмотре он подписан именно так.
|
|
Assert.Contains(result.Value.Slots, s => s.Block == "конец вещания");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Preview_FailsWhenChannelIsGone()
|
|
{
|
|
var fixture = new TestDb();
|
|
await using var db = fixture.New();
|
|
|
|
var result = await new PreviewGeneratedGridQueryHandler(
|
|
new GridPlanner(db, GroupServices.Dynamic(db), new GroupElementResolver(db))
|
|
).Handle(
|
|
new PreviewGeneratedGridQuery(
|
|
Guid.NewGuid(),
|
|
GridProfileKind.Mixed,
|
|
GridGenerationMode.FillGaps
|
|
),
|
|
CancellationToken.None
|
|
);
|
|
|
|
Assert.False(result.IsSuccess);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validator_RejectsUnknownProfileAndMode()
|
|
{
|
|
var validator = new GenerateGridCommandValidator();
|
|
|
|
Assert.True(
|
|
validator
|
|
.Validate(
|
|
new GenerateGridCommand(
|
|
Guid.NewGuid(),
|
|
GridProfileKind.Sitcom,
|
|
GridGenerationMode.Rebuild
|
|
)
|
|
)
|
|
.IsValid
|
|
);
|
|
// Значения enum приходят с клиента строкой — за пределами каталога они бессмысленны.
|
|
Assert.False(
|
|
validator
|
|
.Validate(
|
|
new GenerateGridCommand(
|
|
Guid.NewGuid(),
|
|
(GridProfileKind)42,
|
|
GridGenerationMode.FillGaps
|
|
)
|
|
)
|
|
.IsValid
|
|
);
|
|
}
|
|
|
|
/// <summary>Канал с одной группой мягкого рейтинга — чтобы детскому профилю было чем закрыться.</summary>
|
|
private static async Task<Guid> SeedAsync(TestDb fixture)
|
|
{
|
|
var show = Show.Create("Мультсериал", ShowKind.Series);
|
|
show.SetAudience(ShowAudience.G);
|
|
var assets = new List<MediaAsset>();
|
|
|
|
for (var i = 0; i < 20; i++)
|
|
{
|
|
var asset = MediaAsset.Register($"e{i}.mkv", ".mkv", MediaSource.Upload);
|
|
asset.MarkProcessing();
|
|
asset.MarkReady(
|
|
new MediaReadyInfo(
|
|
TimeSpan.FromMinutes(22),
|
|
6,
|
|
220,
|
|
1920,
|
|
1080,
|
|
"h264",
|
|
"aac",
|
|
$"assets/e{i}"
|
|
)
|
|
);
|
|
assets.Add(asset);
|
|
show.AddEpisode(asset.Id);
|
|
}
|
|
|
|
var group = Group.Create("Мультфильмы");
|
|
group.AddElement(GroupElementKind.Show, show.Id);
|
|
|
|
var channel = Channel.Create("Детский", "kids", T0);
|
|
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
|
|
channel.SetTemplate(template.Id);
|
|
|
|
await using var seed = fixture.New();
|
|
seed.MediaAssets.AddRange(assets);
|
|
seed.Shows.Add(show);
|
|
seed.Groups.Add(group);
|
|
seed.Channels.Add(channel);
|
|
seed.ScheduleTemplates.Add(template);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
|
|
return channel.Id;
|
|
}
|
|
}
|