test: cover Telegram landing promise smoke
This commit is contained in:
@@ -6,7 +6,7 @@ on:
|
||||
- main
|
||||
|
||||
env:
|
||||
VERSION: 1.9.8
|
||||
VERSION: 1.9.9
|
||||
|
||||
jobs:
|
||||
# ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.9.7</Version>
|
||||
<Version>1.9.9</Version>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Проект разработан с упором на производительность, архитектуру Vertical Slice, Native AOT (для бота) и удобство развертывания с использованием .NET Aspire.
|
||||
|
||||
**Текущая версия:** `v1.9.8`.
|
||||
**Текущая версия:** `v1.9.9`.
|
||||
|
||||
---
|
||||
|
||||
@@ -212,5 +212,17 @@ Owner и co-GM могут открыть мобильный dashboard прямо
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
Основной набор проверок запускается командой:
|
||||
|
||||
```bash
|
||||
dotnet test tests/GmRelay.Bot.Tests/GmRelay.Bot.Tests.csproj --collect:"XPlat Code Coverage"
|
||||
```
|
||||
|
||||
Начиная с `v1.9.9`, тестовый набор включает функциональный smoke-сценарий обещаний лендинга для Telegram: batch-сессии на несколько дат, inline-кнопки записи/выхода, лимиты мест, waitlist, автоповышение игрока, голосование за перенос, direct-notification mode и перерисовку Telegram batch-поста после dashboard-изменений. Smoke работает через fake Telegram messenger и не требует внешнего Telegram API.
|
||||
|
||||
---
|
||||
|
||||
## 📜 Лицензия
|
||||
Проект распространяется под лицензией MIT. Использование в некоммерческих целях приветствуется.
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@ services:
|
||||
retries: 10
|
||||
|
||||
bot:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-bot:1.9.7
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-bot:1.9.9
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
@@ -30,7 +30,7 @@ services:
|
||||
- gmrelay
|
||||
|
||||
web:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-web:1.9.7
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-web:1.9.9
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="nav-version">v1.9.6</div>
|
||||
<div class="nav-version">v1.9.9</div>
|
||||
</div>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* ============================================
|
||||
GM-Relay Design System v1.9.6
|
||||
GM-Relay Design System v1.9.9
|
||||
Dark RPG Dashboard Theme
|
||||
============================================ */
|
||||
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
using GmRelay.Bot.Features.Sessions.CreateSession;
|
||||
using GmRelay.Bot.Features.Sessions.RescheduleSession;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Rendering;
|
||||
using Telegram.Bot.Types.ReplyMarkups;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Features.Landing;
|
||||
|
||||
public sealed class TelegramLandingPromisesSmokeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Smoke_ShouldCoverTelegramLandingPromisesWithoutExternalTelegramApi()
|
||||
{
|
||||
var nowUtc = new DateTimeOffset(2026, 5, 1, 12, 0, 0, TimeSpan.Zero);
|
||||
var parseResult = NewSessionCommandParser.Parse(BuildRecurringSessionCommand(), nowUtc);
|
||||
|
||||
Assert.True(parseResult.IsValid);
|
||||
Assert.Equal(3, parseResult.ScheduledTimes.Count);
|
||||
Assert.Equal(2, parseResult.MaxPlayers);
|
||||
Assert.Equal(TimeSpan.FromDays(7), parseResult.ScheduledTimes[1] - parseResult.ScheduledTimes[0]);
|
||||
|
||||
var scenario = TelegramLandingSmokeScenario.Publish(parseResult, SessionNotificationMode.GroupAndDirect);
|
||||
var publishedCallbacks = CallbackData(scenario.LastMessage.Markup);
|
||||
|
||||
Assert.Contains("Landing Promise Smoke", scenario.LastMessage.Text);
|
||||
Assert.Equal(6, publishedCallbacks.Count);
|
||||
Assert.All(scenario.Sessions, session =>
|
||||
{
|
||||
Assert.Contains($"join_session:{session.Id}", publishedCallbacks);
|
||||
Assert.Contains($"leave_session:{session.Id}", publishedCallbacks);
|
||||
Assert.Contains(session.ScheduledAt.FormatMoscow(), scenario.LastMessage.Text);
|
||||
});
|
||||
|
||||
var firstSessionId = scenario.Sessions[0].Id;
|
||||
var alice = scenario.Join(firstSessionId, 1001, "Alice", "alice");
|
||||
var bob = scenario.Join(firstSessionId, 1002, "Bob", "bob");
|
||||
var carol = scenario.Join(firstSessionId, 1003, "Carol", "carol");
|
||||
|
||||
Assert.Equal(ParticipantRegistrationStatus.Active, scenario.RegistrationStatus(firstSessionId, alice));
|
||||
Assert.Equal(ParticipantRegistrationStatus.Active, scenario.RegistrationStatus(firstSessionId, bob));
|
||||
Assert.Equal(ParticipantRegistrationStatus.Waitlisted, scenario.RegistrationStatus(firstSessionId, carol));
|
||||
Assert.Contains("2/2", scenario.LastMessage.Text);
|
||||
Assert.Contains("@alice", scenario.LastMessage.Text);
|
||||
Assert.Contains("@bob", scenario.LastMessage.Text);
|
||||
Assert.Contains("@carol", scenario.LastMessage.Text);
|
||||
|
||||
scenario.Leave(firstSessionId, alice);
|
||||
|
||||
Assert.False(scenario.HasParticipant(firstSessionId, alice));
|
||||
Assert.Equal(ParticipantRegistrationStatus.Active, scenario.RegistrationStatus(firstSessionId, carol));
|
||||
Assert.DoesNotContain("@alice", scenario.LastMessage.Text);
|
||||
Assert.Contains("@carol", scenario.LastMessage.Text);
|
||||
|
||||
scenario.MarkRsvpConfirmed(firstSessionId, bob);
|
||||
scenario.MarkRsvpConfirmed(firstSessionId, carol);
|
||||
|
||||
var option1Id = Guid.NewGuid();
|
||||
var option2Id = Guid.NewGuid();
|
||||
var options = new[]
|
||||
{
|
||||
new RescheduleOptionDto(option1Id, 1, new DateTimeOffset(2026, 5, 29, 16, 30, 0, TimeSpan.Zero)),
|
||||
new RescheduleOptionDto(option2Id, 2, new DateTimeOffset(2026, 5, 30, 15, 0, 0, TimeSpan.Zero))
|
||||
};
|
||||
var deadline = new DateTimeOffset(2026, 5, 20, 18, 0, 0, TimeSpan.Zero);
|
||||
var voteParticipants = scenario.ActiveVoteParticipants(firstSessionId);
|
||||
var voteMessage = HandleRescheduleTimeInputHandler.BuildVotingMessage(
|
||||
scenario.Title,
|
||||
scenario.Sessions[0].ScheduledAt,
|
||||
deadline,
|
||||
options,
|
||||
voteParticipants,
|
||||
[]);
|
||||
var voteKeyboard = HandleRescheduleTimeInputHandler.BuildVotingKeyboard(options);
|
||||
|
||||
Assert.Contains("Landing Promise Smoke", voteMessage);
|
||||
Assert.Contains("0/2", voteMessage);
|
||||
Assert.Contains($"reschedule_vote:{option1Id}", CallbackData(voteKeyboard));
|
||||
Assert.Contains($"reschedule_vote:{option2Id}", CallbackData(voteKeyboard));
|
||||
|
||||
var votes = voteParticipants
|
||||
.Select(participant => new RescheduleOptionVoteDto(
|
||||
option2Id,
|
||||
participant.PlayerId,
|
||||
participant.DisplayName,
|
||||
participant.TelegramUsername))
|
||||
.ToList();
|
||||
var decision = RescheduleVoteRules.SelectWinner(
|
||||
options.Select(option => new RescheduleOptionVoteCount(
|
||||
option.OptionId,
|
||||
votes.Count(vote => vote.OptionId == option.OptionId))).ToList());
|
||||
|
||||
Assert.Equal(RescheduleVoteOutcome.Approved, decision.Outcome);
|
||||
Assert.Equal(option2Id, decision.SelectedOptionId);
|
||||
|
||||
scenario.ApplyReschedule(firstSessionId, options[1].ProposedAt);
|
||||
|
||||
Assert.Equal(options[1].ProposedAt.UtcDateTime, scenario.Sessions[0].ScheduledAt);
|
||||
Assert.Equal(RsvpStatus.Pending, scenario.RsvpStatus(firstSessionId, bob));
|
||||
Assert.Equal(RsvpStatus.Pending, scenario.RsvpStatus(firstSessionId, carol));
|
||||
Assert.Contains(options[1].ProposedAt.UtcDateTime.FormatMoscow(), scenario.LastMessage.Text);
|
||||
Assert.Collection(
|
||||
scenario.Messenger.DirectMessages.Select(message => message.TelegramId).Order(),
|
||||
telegramId => Assert.Equal(1002, telegramId),
|
||||
telegramId => Assert.Equal(1003, telegramId));
|
||||
Assert.All(scenario.Messenger.DirectMessages, message =>
|
||||
{
|
||||
Assert.Contains("Landing Promise Smoke", message.Text);
|
||||
Assert.Contains(options[1].ProposedAt.UtcDateTime.FormatMoscow(), message.Text);
|
||||
});
|
||||
|
||||
var editsBeforeDashboardUpdate = scenario.Messenger.Edits.Count;
|
||||
|
||||
scenario.UpdateBatchFromDashboard("Landing Promise Smoke - Dashboard Sync");
|
||||
|
||||
Assert.True(scenario.Messenger.Edits.Count > editsBeforeDashboardUpdate);
|
||||
Assert.Contains("Landing Promise Smoke - Dashboard Sync", scenario.LastMessage.Text);
|
||||
Assert.Contains("@bob", scenario.LastMessage.Text);
|
||||
Assert.Contains("@carol", scenario.LastMessage.Text);
|
||||
}
|
||||
|
||||
private static string BuildRecurringSessionCommand() =>
|
||||
string.Join(
|
||||
'\n',
|
||||
"/newsession",
|
||||
"\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435: Landing Promise Smoke",
|
||||
"\u0412\u0440\u0435\u043c\u044f: 15.05.2026 19:30",
|
||||
"\u0418\u0433\u0440: 3",
|
||||
"\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b: 7",
|
||||
"\u041c\u0435\u0441\u0442: 2",
|
||||
"\u0421\u0441\u044b\u043b\u043a\u0430: https://example.test/table");
|
||||
|
||||
private static IReadOnlyList<string> CallbackData(InlineKeyboardMarkup markup) =>
|
||||
markup.InlineKeyboard
|
||||
.SelectMany(row => row)
|
||||
.Select(button => button.CallbackData)
|
||||
.OfType<string>()
|
||||
.ToList();
|
||||
|
||||
private sealed class TelegramLandingSmokeScenario
|
||||
{
|
||||
private readonly List<SmokeSession> sessions;
|
||||
private readonly List<SmokeParticipant> participants = [];
|
||||
private readonly SessionNotificationMode notificationMode;
|
||||
|
||||
private TelegramLandingSmokeScenario(
|
||||
string title,
|
||||
IReadOnlyList<DateTimeOffset> scheduledTimes,
|
||||
int? maxPlayers,
|
||||
SessionNotificationMode notificationMode)
|
||||
{
|
||||
Title = title;
|
||||
this.notificationMode = notificationMode;
|
||||
sessions = scheduledTimes
|
||||
.Select(scheduledAt => new SmokeSession(
|
||||
Guid.NewGuid(),
|
||||
scheduledAt.UtcDateTime,
|
||||
SessionStatus.Planned,
|
||||
maxPlayers))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public string Title { get; private set; }
|
||||
public FakeTelegramMessenger Messenger { get; } = new();
|
||||
public IReadOnlyList<SmokeSession> Sessions => sessions;
|
||||
public FakeTelegramMessage LastMessage => Messenger.LastMessage;
|
||||
|
||||
public static TelegramLandingSmokeScenario Publish(
|
||||
NewSessionParseResult parseResult,
|
||||
SessionNotificationMode notificationMode)
|
||||
{
|
||||
var scenario = new TelegramLandingSmokeScenario(
|
||||
parseResult.Title!,
|
||||
parseResult.ScheduledTimes,
|
||||
parseResult.MaxPlayers,
|
||||
notificationMode);
|
||||
|
||||
scenario.RenderBatch();
|
||||
return scenario;
|
||||
}
|
||||
|
||||
public Guid Join(Guid sessionId, long telegramId, string displayName, string? telegramUsername)
|
||||
{
|
||||
var session = sessions.Single(value => value.Id == sessionId);
|
||||
var activeParticipants = participants.Count(participant =>
|
||||
participant.SessionId == sessionId &&
|
||||
participant.RegistrationStatus == ParticipantRegistrationStatus.Active);
|
||||
var registrationStatus = SessionCapacityRules.DecideJoinStatus(
|
||||
session.MaxPlayers,
|
||||
activeParticipants);
|
||||
var playerId = Guid.NewGuid();
|
||||
|
||||
participants.Add(new SmokeParticipant(
|
||||
sessionId,
|
||||
playerId,
|
||||
telegramId,
|
||||
displayName,
|
||||
telegramUsername,
|
||||
registrationStatus,
|
||||
GmRelay.Shared.Domain.RsvpStatus.Pending,
|
||||
participants.Count));
|
||||
|
||||
RenderBatch();
|
||||
return playerId;
|
||||
}
|
||||
|
||||
public void Leave(Guid sessionId, Guid playerId)
|
||||
{
|
||||
var participant = participants.Single(value =>
|
||||
value.SessionId == sessionId &&
|
||||
value.PlayerId == playerId);
|
||||
participants.Remove(participant);
|
||||
|
||||
var session = sessions.Single(value => value.Id == sessionId);
|
||||
var activeParticipantsAfterLeave = participants.Count(value =>
|
||||
value.SessionId == sessionId &&
|
||||
value.RegistrationStatus == ParticipantRegistrationStatus.Active);
|
||||
var waitlistedParticipants = participants.Count(value =>
|
||||
value.SessionId == sessionId &&
|
||||
value.RegistrationStatus == ParticipantRegistrationStatus.Waitlisted);
|
||||
|
||||
if (SessionCapacityRules.ShouldPromoteAfterParticipantLeaves(
|
||||
participant.RegistrationStatus,
|
||||
session.MaxPlayers,
|
||||
activeParticipantsAfterLeave,
|
||||
waitlistedParticipants))
|
||||
{
|
||||
var promoted = participants
|
||||
.Where(value =>
|
||||
value.SessionId == sessionId &&
|
||||
value.RegistrationStatus == ParticipantRegistrationStatus.Waitlisted)
|
||||
.OrderBy(value => value.JoinOrder)
|
||||
.First();
|
||||
|
||||
promoted.RegistrationStatus = ParticipantRegistrationStatus.Active;
|
||||
promoted.RsvpStatus = GmRelay.Shared.Domain.RsvpStatus.Pending;
|
||||
}
|
||||
|
||||
RenderBatch();
|
||||
}
|
||||
|
||||
public bool HasParticipant(Guid sessionId, Guid playerId) =>
|
||||
participants.Any(value => value.SessionId == sessionId && value.PlayerId == playerId);
|
||||
|
||||
public string RegistrationStatus(Guid sessionId, Guid playerId) =>
|
||||
participants.Single(value =>
|
||||
value.SessionId == sessionId &&
|
||||
value.PlayerId == playerId).RegistrationStatus;
|
||||
|
||||
public string RsvpStatus(Guid sessionId, Guid playerId) =>
|
||||
participants.Single(value =>
|
||||
value.SessionId == sessionId &&
|
||||
value.PlayerId == playerId).RsvpStatus;
|
||||
|
||||
public void MarkRsvpConfirmed(Guid sessionId, Guid playerId)
|
||||
{
|
||||
participants.Single(value =>
|
||||
value.SessionId == sessionId &&
|
||||
value.PlayerId == playerId).RsvpStatus = GmRelay.Shared.Domain.RsvpStatus.Confirmed;
|
||||
}
|
||||
|
||||
public IReadOnlyList<VoteParticipantDto> ActiveVoteParticipants(Guid sessionId) =>
|
||||
participants
|
||||
.Where(value =>
|
||||
value.SessionId == sessionId &&
|
||||
value.RegistrationStatus == ParticipantRegistrationStatus.Active)
|
||||
.OrderBy(value => value.DisplayName)
|
||||
.Select(value => new VoteParticipantDto(
|
||||
value.PlayerId,
|
||||
value.DisplayName,
|
||||
value.TelegramUsername,
|
||||
value.TelegramId))
|
||||
.ToList();
|
||||
|
||||
public void ApplyReschedule(Guid sessionId, DateTimeOffset newScheduledAt)
|
||||
{
|
||||
sessions.Single(value => value.Id == sessionId).ScheduledAt = newScheduledAt.UtcDateTime;
|
||||
|
||||
foreach (var participant in participants.Where(value =>
|
||||
value.SessionId == sessionId &&
|
||||
value.RegistrationStatus == ParticipantRegistrationStatus.Active))
|
||||
{
|
||||
participant.RsvpStatus = GmRelay.Shared.Domain.RsvpStatus.Pending;
|
||||
}
|
||||
|
||||
RenderBatch();
|
||||
|
||||
if (!notificationMode.ShouldSendDirectMessages())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var notification = $"""
|
||||
Reschedule approved
|
||||
{Title}
|
||||
{newScheduledAt.UtcDateTime.FormatMoscow()}
|
||||
""";
|
||||
foreach (var participant in participants.Where(value =>
|
||||
value.SessionId == sessionId &&
|
||||
value.RegistrationStatus == ParticipantRegistrationStatus.Active))
|
||||
{
|
||||
Messenger.SendDirectMessage(participant.TelegramId, notification);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateBatchFromDashboard(string title)
|
||||
{
|
||||
Title = title;
|
||||
RenderBatch();
|
||||
}
|
||||
|
||||
private void RenderBatch()
|
||||
{
|
||||
var renderResult = SessionBatchRenderer.Render(
|
||||
Title,
|
||||
sessions
|
||||
.Select(session => new SessionBatchDto(
|
||||
session.Id,
|
||||
session.ScheduledAt,
|
||||
session.Status,
|
||||
session.MaxPlayers))
|
||||
.ToList(),
|
||||
participants
|
||||
.Select(participant => new ParticipantBatchDto(
|
||||
participant.SessionId,
|
||||
participant.DisplayName,
|
||||
participant.TelegramUsername,
|
||||
participant.RegistrationStatus))
|
||||
.ToList());
|
||||
|
||||
if (Messenger.HasPublishedMessage)
|
||||
{
|
||||
Messenger.EditBatchMessage(renderResult.Text, renderResult.Markup);
|
||||
}
|
||||
else
|
||||
{
|
||||
Messenger.PublishBatchMessage(renderResult.Text, renderResult.Markup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeTelegramMessenger
|
||||
{
|
||||
private const int BatchMessageId = 7001;
|
||||
|
||||
public List<FakeTelegramMessage> Sends { get; } = [];
|
||||
public List<FakeTelegramMessage> Edits { get; } = [];
|
||||
public List<FakeDirectMessage> DirectMessages { get; } = [];
|
||||
public bool HasPublishedMessage => Sends.Count > 0;
|
||||
public FakeTelegramMessage LastMessage => Edits.Count > 0 ? Edits[^1] : Sends[^1];
|
||||
|
||||
public void PublishBatchMessage(string text, InlineKeyboardMarkup markup) =>
|
||||
Sends.Add(new FakeTelegramMessage(BatchMessageId, text, markup));
|
||||
|
||||
public void EditBatchMessage(string text, InlineKeyboardMarkup markup) =>
|
||||
Edits.Add(new FakeTelegramMessage(BatchMessageId, text, markup));
|
||||
|
||||
public void SendDirectMessage(long telegramId, string text) =>
|
||||
DirectMessages.Add(new FakeDirectMessage(telegramId, text));
|
||||
}
|
||||
|
||||
private sealed record FakeTelegramMessage(
|
||||
int MessageId,
|
||||
string Text,
|
||||
InlineKeyboardMarkup Markup);
|
||||
|
||||
private sealed record FakeDirectMessage(long TelegramId, string Text);
|
||||
|
||||
private sealed record SmokeSession(
|
||||
Guid Id,
|
||||
DateTime ScheduledAt,
|
||||
string Status,
|
||||
int? MaxPlayers)
|
||||
{
|
||||
public DateTime ScheduledAt { get; set; } = ScheduledAt;
|
||||
}
|
||||
|
||||
private sealed record SmokeParticipant(
|
||||
Guid SessionId,
|
||||
Guid PlayerId,
|
||||
long TelegramId,
|
||||
string DisplayName,
|
||||
string? TelegramUsername,
|
||||
string RegistrationStatus,
|
||||
string RsvpStatus,
|
||||
int JoinOrder)
|
||||
{
|
||||
public string RegistrationStatus { get; set; } = RegistrationStatus;
|
||||
public string RsvpStatus { get; set; } = RsvpStatus;
|
||||
}
|
||||
}
|
||||
@@ -655,6 +655,10 @@ public sealed class AuthorizedSessionServiceTests
|
||||
public string? LastAddedCoGmUsername { get; private set; }
|
||||
public Guid? LastRemovedCoGmGroupId { get; private set; }
|
||||
public long? LastRemovedCoGmTelegramId { get; private set; }
|
||||
public bool RemovePlayerCalled { get; private set; }
|
||||
public Guid? LastRemovedPlayerSessionId { get; private set; }
|
||||
public Guid? LastRemovedPlayerGroupId { get; private set; }
|
||||
public Guid? LastRemovedPlayerParticipantId { get; private set; }
|
||||
|
||||
public Task<List<WebGameGroup>> GetGroupsForGmAsync(long gmId) =>
|
||||
Task.FromResult(groupsById.Values.Where(group => IsManager(group.Id, gmId)).ToList());
|
||||
@@ -870,6 +874,18 @@ public sealed class AuthorizedSessionServiceTests
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<List<WebParticipant>> GetSessionParticipantsAsync(Guid sessionId) =>
|
||||
Task.FromResult(new List<WebParticipant>());
|
||||
|
||||
public Task RemovePlayerFromSessionAsync(Guid sessionId, Guid groupId, Guid participantId)
|
||||
{
|
||||
RemovePlayerCalled = true;
|
||||
LastRemovedPlayerSessionId = sessionId;
|
||||
LastRemovedPlayerGroupId = groupId;
|
||||
LastRemovedPlayerParticipantId = participantId;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool IsManager(Guid groupId, long telegramId) =>
|
||||
IsOwner(groupId, telegramId) ||
|
||||
managers.Any(manager => manager.GroupId == groupId && manager.TelegramId == telegramId);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace GmRelay.Bot.Tests.Web;
|
||||
|
||||
public sealed class CampaignTemplatesNavigationTests
|
||||
@@ -11,6 +13,20 @@ public sealed class CampaignTemplatesNavigationTests
|
||||
Assert.Contains("Шаблоны", navMenu, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NavMenu_ShouldExposeCurrentProjectVersion()
|
||||
{
|
||||
var navMenu = await File.ReadAllTextAsync(FindRepositoryFile("src/GmRelay.Web/Components/Layout/NavMenu.razor"));
|
||||
var props = XDocument.Load(FindRepositoryFile("Directory.Build.props"));
|
||||
var version = props.Root?
|
||||
.Element("PropertyGroup")?
|
||||
.Element("Version")?
|
||||
.Value;
|
||||
|
||||
Assert.False(string.IsNullOrWhiteSpace(version));
|
||||
Assert.Contains($"v{version}", navMenu, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NavMenuStyles_ShouldStyleNavLinkAnchorsAsStackedRows()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user