Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0a84965b3 | |||
| 67e8d5b558 | |||
| 593f8a62fb | |||
| aee0ac1e6c | |||
| 68945d931f |
@@ -6,7 +6,7 @@ on:
|
||||
- main
|
||||
|
||||
env:
|
||||
VERSION: 3.9.3
|
||||
VERSION: 3.9.5
|
||||
|
||||
jobs:
|
||||
# ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>3.9.3</Version>
|
||||
<Version>3.9.5</Version>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
+3
-3
@@ -49,7 +49,7 @@ services:
|
||||
crond -f
|
||||
|
||||
bot:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-bot:3.9.3
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-bot:3.9.5
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
@@ -67,7 +67,7 @@ services:
|
||||
retries: 3
|
||||
|
||||
discord:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.9.3
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.9.5
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
@@ -86,7 +86,7 @@ services:
|
||||
retries: 3
|
||||
|
||||
web:
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-web:3.9.3
|
||||
image: git.codeanddice.ru/toutsu/gmrelay-web:3.9.5
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
|
||||
@@ -298,13 +298,25 @@ public sealed class GameCreationWizard
|
||||
: (null, "Неверная длительность"),
|
||||
};
|
||||
|
||||
private static (string?, string?) ApplyCapacityChoice(WizardPayload p, string choice) => choice switch
|
||||
private static (string?, string?) ApplyCapacityChoice(WizardPayload p, string choice)
|
||||
{
|
||||
"waitlist:on" => (WizardStepNames.Visibility, SetWaitlist(p, true)),
|
||||
"waitlist:off" => (WizardStepNames.Visibility, SetWaitlist(p, false)),
|
||||
"no_limit" => (WizardStepNames.Visibility, SetMaxPlayers(p, null)),
|
||||
_ => (null, "Неизвестный выбор"),
|
||||
};
|
||||
if (choice is "no_limit")
|
||||
{
|
||||
return (WizardStepNames.Visibility, SetMaxPlayers(p, null));
|
||||
}
|
||||
|
||||
if (choice is "waitlist:on" or "waitlist:off" && p.Single?.MaxPlayers is null)
|
||||
{
|
||||
return (null, "Сначала введите лимит мест или нажмите «♾ Без лимита»");
|
||||
}
|
||||
|
||||
return choice switch
|
||||
{
|
||||
"waitlist:on" => (WizardStepNames.Visibility, SetWaitlist(p, true)),
|
||||
"waitlist:off" => (WizardStepNames.Visibility, SetWaitlist(p, false)),
|
||||
_ => (null, "Неизвестный выбор"),
|
||||
};
|
||||
}
|
||||
|
||||
private static (string?, string?) ApplyVisibilityChoice(WizardPayload p, string choice) => choice switch
|
||||
{
|
||||
@@ -316,9 +328,15 @@ public sealed class GameCreationWizard
|
||||
};
|
||||
|
||||
private static (string?, string?) ApplyPickClubChoice(WizardPayload p, string choice)
|
||||
=> Guid.TryParse(choice, out var id)
|
||||
? (NextAfterVisibility(p), SetClubId(p, id))
|
||||
: (null, "Неверный идентификатор клуба");
|
||||
{
|
||||
if (!Guid.TryParse(choice, out var id))
|
||||
{
|
||||
return (null, "Неверный идентификатор клуба");
|
||||
}
|
||||
|
||||
var error = SetClubId(p, id);
|
||||
return (NextAfterVisibility(p), error);
|
||||
}
|
||||
|
||||
private static (string?, string?) ApplyPublishChoice(WizardPayload p, string choice) => choice switch
|
||||
{
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="nav-version">v3.9.3</div>
|
||||
<div class="nav-version">v3.9.5</div>
|
||||
</div>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
|
||||
@@ -33,16 +33,18 @@ public sealed class DiscordWizardStepCapacityRenderTests
|
||||
Assert.Contains(labels, l => l.Contains("Без лимита", System.StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderCapacity_NoLimitButton_HasChoiceCustomIdForNoLimit()
|
||||
[Theory]
|
||||
[InlineData(WizardStepNames.Capacity, "wizard:btn:choice:Capacity:no_limit")]
|
||||
[InlineData(WizardStepNames.PoolSlotCapacity, "wizard:btn:choice:PoolSlotCapacity:no_limit")]
|
||||
public void Render_NoLimitButton_HasChoiceCustomIdForNoLimit(string step, string expectedCustomIdPrefix)
|
||||
{
|
||||
var draft = new WizardDraft { Step = WizardStepNames.Capacity };
|
||||
var draft = new WizardDraft { Step = step };
|
||||
var render = DiscordWizardStep.Render(draft, new WizardPayload());
|
||||
|
||||
var buttons = ExtractButtons(render);
|
||||
var noLimit = buttons.SingleOrDefault(b => b.Label?.Contains("Без лимита", System.StringComparison.Ordinal) == true);
|
||||
Assert.NotNull(noLimit);
|
||||
Assert.StartsWith("wizard:btn:choice:Capacity:no_limit", noLimit!.CustomId);
|
||||
Assert.StartsWith(expectedCustomIdPrefix, noLimit!.CustomId);
|
||||
}
|
||||
|
||||
private static System.Collections.Generic.List<string> ExtractButtonLabels(
|
||||
|
||||
+13
-18
@@ -20,9 +20,7 @@ public sealed class GameCreationWizardStepTransitionsTests
|
||||
[InlineData(WizardStepNames.System, "Dnd5e", WizardStepNames.Duration)]
|
||||
// Duration → DateTime (single, no maxPlayers yet)
|
||||
[InlineData(WizardStepNames.Duration, "240", WizardStepNames.DateTime)]
|
||||
// Capacity → Visibility
|
||||
[InlineData(WizardStepNames.Capacity, "waitlist:on", WizardStepNames.Visibility)]
|
||||
[InlineData(WizardStepNames.Capacity, "waitlist:off", WizardStepNames.Visibility)]
|
||||
// Capacity → Visibility (only explicit no-limit can skip numeric capacity)
|
||||
[InlineData(WizardStepNames.Capacity, "no_limit", WizardStepNames.Visibility)]
|
||||
// Visibility → Publish (public, no club)
|
||||
[InlineData(WizardStepNames.Visibility, "public", WizardStepNames.Publish)]
|
||||
@@ -98,13 +96,11 @@ public sealed class GameCreationWizardStepTransitionsTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChoiceCallback_FromMismatchedStep_AdvancesBasedOnCallbackStep()
|
||||
public async Task StaleCapacityWaitlistCallback_WithoutCapacity_StaysOnCurrentStep()
|
||||
{
|
||||
// The wizard's callback parser uses the step encoded in the callback
|
||||
// (not the draft's current step) to drive transitions. So a stale
|
||||
// "Capacity" button pressed while the user is on System will in fact
|
||||
// move the draft forward as if they had pressed it on Capacity. We
|
||||
// lock that behaviour in.
|
||||
// A stale waitlist button from Capacity must not move a draft forward
|
||||
// unless MaxPlayers is already set. Otherwise users can reach Confirm
|
||||
// with a missing capacity and get "Не заполнены поля: лимит мест".
|
||||
var wizard = BuildWizard(out var drafts, out _);
|
||||
var draft = NewDraft(WizardStepNames.System);
|
||||
drafts.Seed(draft);
|
||||
@@ -112,17 +108,13 @@ public sealed class GameCreationWizardStepTransitionsTests
|
||||
var data = WizardCallbackData.Choice(WizardStepNames.Capacity, "waitlist:on");
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Visibility, draft.Step);
|
||||
Assert.Equal(WizardStepNames.System, draft.Step);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PickClub_ValidGuid_ReachesStableStep()
|
||||
public async Task PickClub_ValidGuid_AdvancesToPublishOnFirstClick()
|
||||
{
|
||||
// The wizard has a quirk: NextAfterVisibility is evaluated before
|
||||
// SetClubId, so a single click leaves the draft still on PickClub.
|
||||
// We assert that the wizard does NOT throw and the messenger is asked
|
||||
// to re-render (i.e. the handler ran end-to-end).
|
||||
var wizard = BuildWizard(out var drafts, out var messenger);
|
||||
var wizard = BuildWizard(out var drafts, out _);
|
||||
var clubId = Guid.NewGuid();
|
||||
var payload = new WizardPayload
|
||||
{
|
||||
@@ -138,8 +130,11 @@ public sealed class GameCreationWizardStepTransitionsTests
|
||||
var data = WizardCallbackData.Choice(WizardStepNames.PickClub, clubId.ToString());
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
// Wizard acknowledged the callback and re-rendered the (still PickClub) step.
|
||||
Assert.NotEmpty(messenger.Edits);
|
||||
Assert.Equal(WizardStepNames.Publish, draft.Step);
|
||||
using var doc = JsonDocument.Parse(draft.PayloadJson);
|
||||
var root = doc.RootElement;
|
||||
Assert.True(root.TryGetProperty("clubId", out var clubIdJson));
|
||||
Assert.Equal(clubId, clubIdJson.GetGuid());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+23
@@ -136,6 +136,29 @@ public sealed class GameCreationWizardValidationTests
|
||||
Assert.Equal(WizardStepNames.Capacity, draft.Step);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("waitlist:on")]
|
||||
[InlineData("waitlist:off")]
|
||||
public async Task WaitlistChoiceWithoutCapacity_StaysOnCapacityStep(string choice)
|
||||
{
|
||||
var wizard = BuildWizard(out var drafts, out _);
|
||||
var draft = NewDraft(WizardStepNames.Capacity,
|
||||
new WizardPayload
|
||||
{
|
||||
Type = WizardCreationType.Single,
|
||||
Title = "T",
|
||||
System = "Dnd5e",
|
||||
DurationMinutes = 240,
|
||||
Single = new WizardSingleInput { ScheduledAt = DateTimeOffset.UtcNow.AddDays(1) },
|
||||
});
|
||||
drafts.Seed(draft);
|
||||
|
||||
var data = WizardCallbackData.Choice(WizardStepNames.Capacity, choice);
|
||||
await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None);
|
||||
|
||||
Assert.Equal(WizardStepNames.Capacity, draft.Step);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("0")]
|
||||
[InlineData("13")]
|
||||
|
||||
@@ -14,8 +14,16 @@ public sealed class CampaignTemplatesNavigationTests
|
||||
[Fact]
|
||||
public async Task NavMenu_ShouldExposeCurrentProjectVersion()
|
||||
{
|
||||
// Read the version from Directory.Build.props (the canonical source of
|
||||
// truth) so the test doesn't need to be hand-edited on every version
|
||||
// bump. Asserting the rendered NavMenu matches the canonical version
|
||||
// catches real bugs (e.g. someone bumps Directory.Build.props but
|
||||
// forgets to update NavMenu.razor) without false alarms from a stale
|
||||
// hard-coded literal.
|
||||
var propsPath = FindRepositoryFile("Directory.Build.props");
|
||||
var version = ReadVersionFromProps(propsPath);
|
||||
var navMenu = await File.ReadAllTextAsync(FindRepositoryFile("src/GmRelay.Web/Components/Layout/NavMenu.razor"));
|
||||
Assert.Contains("v3.9.3", navMenu, StringComparison.Ordinal);
|
||||
Assert.Contains($"v{version}", navMenu, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -68,4 +76,23 @@ public sealed class CampaignTemplatesNavigationTests
|
||||
|
||||
throw new FileNotFoundException($"Could not locate repository file '{relativePath}'.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse the <c><Version>...</Version></c> element from
|
||||
/// <c>Directory.Build.props</c>. Tolerant of whitespace, comments and
|
||||
/// attribute shuffling — the MSBuild schema for <c>Version</c> is just
|
||||
/// a plain element with a string body.
|
||||
/// </summary>
|
||||
private static string ReadVersionFromProps(string propsPath)
|
||||
{
|
||||
var doc = System.Xml.Linq.XDocument.Load(propsPath);
|
||||
var versionElement = doc.Descendants()
|
||||
.FirstOrDefault(e => e.Name.LocalName == "Version");
|
||||
Assert.NotNull(versionElement);
|
||||
var version = versionElement!.Value.Trim();
|
||||
Assert.False(
|
||||
string.IsNullOrEmpty(version),
|
||||
$"<Version> in {propsPath} is empty");
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user