Files
meeting-assistant/MeetingAssistant.Tests/TaskbarIconTests.cs
T
codex aa42e8edda
PR and Push Build/Test / build-and-test (push) Successful in 10m51s
refactor: tighten tray menu section handling
2026-08-04 10:11:43 +02:00

253 lines
9.3 KiB
C#

using MeetingAssistant.LaunchProfiles;
using MeetingAssistant.Recording;
using MeetingAssistant.Taskbar;
using System.Drawing;
using System.Runtime.Versioning;
namespace MeetingAssistant.Tests;
public sealed class TaskbarIconTests
{
[Fact]
public void IdleMenuOffersStartForEachConfiguredProfile()
{
var menu = MeetingTaskbarMenuBuilder.Build(
Status(),
[Profile("default", "Ctrl+Alt+M"), Profile("english", "Ctrl+Alt+L")],
[new MicrophoneDevice("integrated", "integrated microphone")],
"integrated");
Assert.Equal(RecordingProcessState.Idle, menu.State);
AssertMenuLayout(
menu,
("Open agent", MeetingTaskbarAction.EditRules, false),
("Microphone", MeetingTaskbarAction.OpenSubmenu, true),
("Start meeting recording (default)\tCtrl+Alt+M", MeetingTaskbarAction.StartRecording, false),
("Start meeting recording (english)\tCtrl+Alt+L", MeetingTaskbarAction.StartRecording, false),
("Exit", MeetingTaskbarAction.Exit, true));
Assert.Equal(
["default", "english"],
menu.Items
.Where(item => item.Action == MeetingTaskbarAction.StartRecording)
.Select(item => item.ProfileName));
}
[Fact]
public void MenuOffersMicrophoneSubmenuWithEffectiveDeviceChecked()
{
var menu = MeetingTaskbarMenuBuilder.Build(
Status(),
[Profile("default")],
[
new MicrophoneDevice("integrated", "integrated microphone"),
new MicrophoneDevice("other", "other microphone")
],
"integrated");
var microphoneMenu = Assert.Single(menu.Items, item => item.Text == "Microphone");
Assert.Equal(MeetingTaskbarAction.OpenSubmenu, microphoneMenu.Action);
Assert.NotNull(microphoneMenu.Items);
Assert.Contains(microphoneMenu.Items, item =>
item.Action == MeetingTaskbarAction.SelectMicrophone &&
item.MicrophoneDeviceId == "integrated" &&
item.Text == "integrated microphone" &&
item.IsChecked);
Assert.Contains(microphoneMenu.Items, item =>
item.Action == MeetingTaskbarAction.SelectMicrophone &&
item.MicrophoneDeviceId == "other" &&
item.Text == "other microphone" &&
!item.IsChecked);
}
[Fact]
public void RecordingMenuPrioritizesFinishMeetingInDedicatedSection()
{
var menu = MeetingTaskbarMenuBuilder.Build(
Status(isRecording: true, state: RecordingProcessState.Recording, profile: "default"),
[Profile("default", "Ctrl+Alt+M"), Profile("english", "Ctrl+Alt+L")],
[new MicrophoneDevice("integrated", "integrated microphone")],
"integrated");
Assert.Equal(RecordingProcessState.Recording, menu.State);
AssertMenuLayout(
menu,
("Open agent", MeetingTaskbarAction.EditRules, false),
("Finish meeting", MeetingTaskbarAction.StopRecording, true),
("Microphone", MeetingTaskbarAction.OpenSubmenu, true),
("Cancel meeting recording and discard", MeetingTaskbarAction.AbortRecording, false),
("Switch to english\tCtrl+Alt+L", MeetingTaskbarAction.SwitchProfile, false),
("Exit", MeetingTaskbarAction.Exit, true));
Assert.Equal(
"english",
Assert.Single(menu.Items, item => item.Action == MeetingTaskbarAction.SwitchProfile).ProfileName);
}
[Fact]
public void RecordingMenuKeepsFinishMeetingIsolatedWithoutMicrophones()
{
var menu = MeetingTaskbarMenuBuilder.Build(
Status(isRecording: true, state: RecordingProcessState.Recording, profile: "default"),
[Profile("default")]);
AssertMenuLayout(
menu,
("Open agent", MeetingTaskbarAction.EditRules, false),
("Finish meeting", MeetingTaskbarAction.StopRecording, true),
("Cancel meeting recording and discard", MeetingTaskbarAction.AbortRecording, true),
("Exit", MeetingTaskbarAction.Exit, true));
}
[Fact]
public void ProcessingMenuShowsSummarizingButAllowsStartingNewRecordings()
{
var menu = MeetingTaskbarMenuBuilder.Build(
Status(state: RecordingProcessState.Summarizing, profile: "default"),
[Profile("default"), Profile("english")]);
Assert.Equal(RecordingProcessState.Summarizing, menu.State);
Assert.Contains(menu.Items, item =>
item.Action == MeetingTaskbarAction.StartRecording &&
item.ProfileName == "default");
Assert.Contains(menu.Items, item =>
item.Action == MeetingTaskbarAction.StartRecording &&
item.ProfileName == "english");
Assert.DoesNotContain(menu.Items, item => item.Action == MeetingTaskbarAction.StopRecording);
Assert.DoesNotContain(menu.Items, item => item.Action == MeetingTaskbarAction.AbortRecording);
}
[Fact]
public void RecordingStateWinsOverOlderSummarizingWork()
{
var menu = MeetingTaskbarMenuBuilder.Build(
Status(isRecording: true, state: RecordingProcessState.Recording, profile: "english"),
[Profile("default"), Profile("english")]);
Assert.Equal(RecordingProcessState.Recording, menu.State);
}
[Theory]
[InlineData(RecordingProcessState.Idle, false)]
[InlineData(RecordingProcessState.Recording, true)]
[InlineData(RecordingProcessState.Summarizing, false)]
public void MenuAlwaysOffersExit(RecordingProcessState state, bool isRecording)
{
var menu = MeetingTaskbarMenuBuilder.Build(
Status(isRecording: isRecording, state: state, profile: "default"),
[Profile("default"), Profile("english")]);
Assert.Contains(menu.Items, item =>
item.Action == MeetingTaskbarAction.Exit &&
item.Text == "Exit");
}
[Theory]
[InlineData(RecordingProcessState.Idle, false)]
[InlineData(RecordingProcessState.Recording, true)]
[InlineData(RecordingProcessState.Summarizing, true)]
public void ExitRequiresConfirmationWhileRecordingOrProcessing(
RecordingProcessState state,
bool requiresConfirmation)
{
Assert.Equal(
requiresConfirmation,
MeetingTaskbarExitPolicy.RequiresConfirmation(state));
}
[Fact]
[SupportedOSPlatform("windows")]
public void TaskbarIconGlyphsAreVisuallyCentered()
{
foreach (var state in new[]
{
RecordingProcessState.Idle,
RecordingProcessState.Recording,
RecordingProcessState.Summarizing
})
{
using var bitmap = TaskbarIconRenderer.RenderBitmap(state);
var bounds = GetVisibleGlyphBounds(bitmap);
Assert.True(
IsCentered(GetCenter(bounds.Left, bounds.Right)),
$"{state} glyph is horizontally off-center: {bounds}");
Assert.True(
IsCentered(GetCenter(bounds.Top, bounds.Bottom)),
$"{state} glyph is vertically off-center: {bounds}");
}
}
private static LaunchProfile Profile(string name, string hotkey = "")
{
return new LaunchProfile(name, new MeetingAssistantOptions
{
Hotkey =
{
Toggle = hotkey
}
});
}
private static void AssertMenuLayout(
MeetingTaskbarMenu menu,
params (string Text, MeetingTaskbarAction Action, bool StartsSection)[] expected)
{
Assert.Equal(
expected,
menu.Items.Select(item => (item.Text, item.Action, item.StartsSection)));
}
private static RecordingStatus Status(
bool isRecording = false,
RecordingProcessState state = RecordingProcessState.Idle,
string? profile = null)
{
return new RecordingStatus(
isRecording,
state == RecordingProcessState.Idle ? null : "transcript.md",
state == RecordingProcessState.Idle ? null : "meeting.md",
state == RecordingProcessState.Idle ? null : "context.md",
state == RecordingProcessState.Idle ? null : "summary.md",
state,
profile);
}
[SupportedOSPlatform("windows")]
private static Rectangle GetVisibleGlyphBounds(Bitmap bitmap)
{
var left = bitmap.Width;
var top = bitmap.Height;
var right = -1;
var bottom = -1;
for (var y = 0; y < bitmap.Height; y++)
{
for (var x = 0; x < bitmap.Width; x++)
{
var pixel = bitmap.GetPixel(x, y);
if (pixel.A < 32 || pixel.GetBrightness() < 0.58f)
{
continue;
}
left = Math.Min(left, x);
top = Math.Min(top, y);
right = Math.Max(right, x);
bottom = Math.Max(bottom, y);
}
}
Assert.True(right >= left && bottom >= top, "Expected the icon to contain a visible white glyph.");
return Rectangle.FromLTRB(left, top, right + 1, bottom + 1);
}
private static double GetCenter(int start, int end)
{
return (start + end) / 2.0;
}
private static bool IsCentered(double center)
{
return center is >= 7.25 and <= 8.75;
}
}