Skip to content

Commit 4decb7b

Browse files
committed
feat(switches): add SwitchEmbedRenderer (embed, control row, pairing prompt)
1 parent bcb45dc commit 4decb7b

2 files changed

Lines changed: 144 additions & 0 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using Discord;
2+
using RustPlusBot.Domain.Switches;
3+
4+
namespace RustPlusBot.Features.Switches.Rendering;
5+
6+
/// <summary>Renders a Smart Switch as a Discord embed + control row, and the pairing-prompt embed + row. Pure.</summary>
7+
/// <param name="localizer">The switch localizer.</param>
8+
internal sealed class SwitchEmbedRenderer(ISwitchLocalizer localizer)
9+
{
10+
/// <summary>Renders the switch embed and its control buttons. <paramref name="isActive"/> null ⇒ unreachable.</summary>
11+
/// <param name="sw">The switch.</param>
12+
/// <param name="isActive">On (true), off (false), or unreachable (null).</param>
13+
/// <param name="culture">The guild culture.</param>
14+
/// <returns>The embed and the component rows.</returns>
15+
public (Embed Embed, MessageComponent Components) RenderSwitch(SmartSwitch sw, bool? isActive, string culture)
16+
{
17+
ArgumentNullException.ThrowIfNull(sw);
18+
var unreachable = isActive is null;
19+
var statusKey = isActive switch
20+
{
21+
true => "switch.status.on",
22+
false => "switch.status.off",
23+
null => "switch.status.unreachable",
24+
};
25+
26+
var embed = new EmbedBuilder()
27+
.WithTitle(sw.Name)
28+
.WithDescription(localizer.Get(statusKey, culture))
29+
.WithFooter(localizer.Get("switch.embed.footer", culture, sw.EntityId))
30+
.Build();
31+
32+
var tail = $"{sw.ServerId}:{sw.EntityId}";
33+
var components = new ComponentBuilder()
34+
.WithButton(localizer.Get("switch.button.on", culture), SwitchComponentIds.OnPrefix + tail,
35+
ButtonStyle.Success, disabled: unreachable || isActive == true)
36+
.WithButton(localizer.Get("switch.button.off", culture), SwitchComponentIds.OffPrefix + tail,
37+
ButtonStyle.Secondary, disabled: unreachable || isActive == false)
38+
.WithButton(localizer.Get("switch.button.strobe", culture), SwitchComponentIds.StrobePrefix + tail,
39+
ButtonStyle.Primary, disabled: unreachable)
40+
.WithButton(localizer.Get("switch.button.rename", culture), SwitchComponentIds.RenamePrefix + tail,
41+
ButtonStyle.Secondary, disabled: unreachable)
42+
.Build();
43+
44+
return (embed, components);
45+
}
46+
47+
/// <summary>Renders the transient "New switch detected — Add it?" prompt.</summary>
48+
/// <param name="serverId">The server id.</param>
49+
/// <param name="entityId">The entity id.</param>
50+
/// <param name="defaultName">The generated default name.</param>
51+
/// <param name="culture">The guild culture.</param>
52+
/// <returns>The prompt embed and Accept/Dismiss row.</returns>
53+
public (Embed Embed, MessageComponent Components) RenderPrompt(
54+
Guid serverId, ulong entityId, string defaultName, string culture)
55+
{
56+
var embed = new EmbedBuilder()
57+
.WithTitle(localizer.Get("switch.prompt.title", culture))
58+
.WithDescription(localizer.Get("switch.prompt.body", culture, defaultName))
59+
.Build();
60+
61+
var tail = $"{serverId}:{entityId}";
62+
var components = new ComponentBuilder()
63+
.WithButton(localizer.Get("switch.prompt.accept", culture), SwitchComponentIds.AcceptPrefix + tail,
64+
ButtonStyle.Success)
65+
.WithButton(localizer.Get("switch.prompt.dismiss", culture), SwitchComponentIds.DismissPrefix + tail,
66+
ButtonStyle.Secondary)
67+
.Build();
68+
69+
return (embed, components);
70+
}
71+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Discord;
2+
using RustPlusBot.Domain.Switches;
3+
using RustPlusBot.Features.Switches.Rendering;
4+
5+
namespace RustPlusBot.Features.Switches.Tests;
6+
7+
public sealed class SwitchEmbedRendererTests
8+
{
9+
private static SwitchEmbedRenderer Create() =>
10+
new(new SwitchLocalizer(SwitchLocalizationCatalog.Default));
11+
12+
private static SmartSwitch Sample(string name = "Front gate", bool lastActive = false) => new()
13+
{
14+
GuildId = 10UL, ServerId = Guid.NewGuid(), EntityId = 42UL, Name = name, LastIsActive = lastActive,
15+
};
16+
17+
[Fact]
18+
public void RenderSwitch_on_shows_on_status_and_off_button_enabled()
19+
{
20+
var (embed, components) = Create().RenderSwitch(Sample(), isActive: true, "en");
21+
22+
Assert.Contains("ON", embed.Description ?? embed.Title ?? string.Empty, StringComparison.Ordinal);
23+
// Find the on/off buttons by custom id and assert disabled-state reflects current value.
24+
var buttons = components.Components.OfType<ActionRowComponent>().SelectMany(r => r.Components).OfType<ButtonComponent>().ToList();
25+
var onBtn = buttons.Single(b => b.CustomId!.StartsWith(SwitchComponentIds.OnPrefix, StringComparison.Ordinal));
26+
var offBtn = buttons.Single(b => b.CustomId!.StartsWith(SwitchComponentIds.OffPrefix, StringComparison.Ordinal));
27+
Assert.True(onBtn.IsDisabled); // already on
28+
Assert.False(offBtn.IsDisabled);
29+
}
30+
31+
[Fact]
32+
public void RenderSwitch_off_shows_off_status_and_on_button_enabled()
33+
{
34+
var (embed, components) = Create().RenderSwitch(Sample(), isActive: false, "en");
35+
36+
Assert.Contains("OFF", embed.Description ?? embed.Title ?? string.Empty, StringComparison.Ordinal);
37+
var buttons = components.Components.OfType<ActionRowComponent>().SelectMany(r => r.Components).OfType<ButtonComponent>().ToList();
38+
var onBtn = buttons.Single(b => b.CustomId!.StartsWith(SwitchComponentIds.OnPrefix, StringComparison.Ordinal));
39+
var offBtn = buttons.Single(b => b.CustomId!.StartsWith(SwitchComponentIds.OffPrefix, StringComparison.Ordinal));
40+
Assert.False(onBtn.IsDisabled);
41+
Assert.True(offBtn.IsDisabled); // already off
42+
}
43+
44+
[Fact]
45+
public void RenderSwitch_unreachable_disables_all_control_buttons()
46+
{
47+
var (embed, components) = Create().RenderSwitch(Sample(), isActive: null, "en");
48+
49+
Assert.Contains("Unreachable", embed.Description ?? string.Empty, StringComparison.Ordinal);
50+
var buttons = components.Components.OfType<ActionRowComponent>().SelectMany(r => r.Components).OfType<ButtonComponent>().ToList();
51+
Assert.All(buttons, b => Assert.True(b.IsDisabled));
52+
}
53+
54+
[Fact]
55+
public void RenderSwitch_french_uses_french_status()
56+
{
57+
var (embed, _) = Create().RenderSwitch(Sample(), isActive: true, "fr");
58+
Assert.Contains("ALLUMÉ", embed.Description ?? string.Empty, StringComparison.Ordinal);
59+
}
60+
61+
[Fact]
62+
public void RenderPrompt_carries_accept_and_dismiss_with_identity_tail()
63+
{
64+
var serverId = Guid.NewGuid();
65+
var (_, components) = Create().RenderPrompt(serverId, 42UL, "Switch 42", "en");
66+
67+
var buttons = components.Components.OfType<ActionRowComponent>().SelectMany(r => r.Components).OfType<ButtonComponent>().ToList();
68+
Assert.Contains(buttons, b =>
69+
b.CustomId == $"{SwitchComponentIds.AcceptPrefix}{serverId}:42");
70+
Assert.Contains(buttons, b =>
71+
b.CustomId == $"{SwitchComponentIds.DismissPrefix}{serverId}:42");
72+
}
73+
}

0 commit comments

Comments
 (0)