Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ jobs:
$wpfClasses = @(
"ClipboardManagerWindowBehaviorTests",
"CommandButtonStyleTests",
"FileSorterWindowBehaviorTests",
"FormControlHeightTests",
"MainWindowIconConverterOrientationTests",
"ZenEditorWindowBehaviorTests",
Expand All @@ -111,6 +112,7 @@ jobs:
$wpfClasses = @(
"ClipboardManagerWindowBehaviorTests",
"CommandButtonStyleTests",
"FileSorterWindowBehaviorTests",
"FormControlHeightTests",
"MainWindowIconConverterOrientationTests",
"ZenEditorWindowBehaviorTests",
Expand Down
63 changes: 63 additions & 0 deletions AiteBar.Tests/ActionServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,69 @@ public async Task StartDownloadsAsync_LaunchesDownloadsShell()
Assert.True(runtime.StartedProcessInfos[0].UseShellExecute);
}

[Theory]
[InlineData("Search")]
[InlineData("Screenshot")]
[InlineData("Record")]
[InlineData("Calculator")]
[InlineData("Explorer")]
[InlineData("Downloads")]
[InlineData("ShowDesktop")]
[InlineData("AppsFolder")]
public async Task SystemUtilityLaunch_WhenRuntimeReturnsNull_ThrowsLocalizedFailure(string utility)
{
var runtime = new FakeActionServiceRuntime();
runtime.ProcessesToReturn.Enqueue(null);
var service = new ActionService(new AppSettingsService(), runtime);

Task launch = utility switch
{
"Search" => service.StartSearchAsync("release review"),
"Screenshot" => service.StartScreenshotAsync(),
"Record" => service.StartRecordVideoAsync(),
"Calculator" => service.StartCalculatorAsync(),
"Explorer" => service.StartExplorerAsync(),
"Downloads" => service.StartDownloadsAsync(),
"ShowDesktop" => service.StartShowDesktopAsync(),
"AppsFolder" => service.StartAppsFolderAsync(),
_ => throw new ArgumentOutOfRangeException(nameof(utility))
};

InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(() => launch);
Assert.False(string.IsNullOrWhiteSpace(exception.Message));
}

[Fact]
public async Task StartCopilotAsync_SendsWinCAndReleasesKeys()
{
var runtime = new FakeActionServiceRuntime();
var service = new ActionService(new AppSettingsService(), runtime);

await service.StartCopilotAsync();

Assert.Equal(4, runtime.SendInputCalls.Count);
Assert.Equal(NativeMethods.VK_LWIN, runtime.SendInputCalls[0][0].U.ki.wVk);
Assert.Equal(0x43, runtime.SendInputCalls[1][0].U.ki.wVk);
Assert.Equal(NativeMethods.KEYEVENTF_KEYUP, runtime.SendInputCalls[2][0].U.ki.dwFlags);
Assert.Equal(NativeMethods.VK_LWIN, runtime.SendInputCalls[3][0].U.ki.wVk);
Assert.Equal(NativeMethods.KEYEVENTF_KEYUP, runtime.SendInputCalls[3][0].U.ki.dwFlags);
}

[Fact]
public async Task StartCopilotAsync_WhenCKeyInjectionFails_ReleasesWinKey()
{
var runtime = new FakeActionServiceRuntime();
runtime.SendInputResults.Enqueue(1);
runtime.SendInputResults.Enqueue(0);
var service = new ActionService(new AppSettingsService(), runtime);

await Assert.ThrowsAsync<InvalidOperationException>(() => service.StartCopilotAsync());

Assert.Equal(3, runtime.SendInputCalls.Count);
Assert.Equal(NativeMethods.VK_LWIN, runtime.SendInputCalls[2][0].U.ki.wVk);
Assert.Equal(NativeMethods.KEYEVENTF_KEYUP, runtime.SendInputCalls[2][0].U.ki.dwFlags);
}



[Fact]
Expand Down
4 changes: 3 additions & 1 deletion AiteBar.Tests/AiProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -531,13 +531,15 @@ public async Task Gateway_LegacyMethods_PreserveExactLegacyRouteOrder()
settingsService.Settings.Ai.Connections[2].PreferredModelId = "apple";
var gateway = new AiGateway(settingsService, client, TimeProvider.System);

await Assert.ThrowsAsync<NoAvailableConnectionException>(() =>
NoAvailableConnectionException exception = await Assert.ThrowsAsync<NoAvailableConnectionException>(() =>
gateway.GenerateAsync(new AiChatRequest
{
Messages = [new AiChatMessage("user", "hello")],
RequireFreeModel = true
}));

Assert.Equal(AiAvailabilityFailureReason.RateLimited, exception.Reason);

Assert.Equal(
[
"alpha:zebra", "gamma:zebra",
Expand Down
64 changes: 64 additions & 0 deletions AiteBar.Tests/AiStreamingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,70 @@ public async Task Gateway_MarksConnectionUnavailableWhenStartedStreamFails()
Assert.Equal(AiConnectionState.Unavailable, gateway.GetConnectionStatus("test")?.State);
}

[Fact]
public async Task TextProcessingGateway_SkipsEmptyStreamAndUsesNextConnection()
{
var credentials = new MemoryCredentialStore();
credentials.Write("AiteBar/AI/first", "key-first");
credentials.Write("AiteBar/AI/second", "key-second");
var generationAttempts = new List<string>();
var handler = new DelegateHandler(request =>
{
if (request.Method == HttpMethod.Get)
{
return Task.FromResult(Json(
"{\"data\":[{\"id\":\"llama-3.3-70b-versatile\",\"name\":\"Llama 3.3 70B\"}]}"));
}

string key = request.Headers.Authorization?.Parameter ?? string.Empty;
generationAttempts.Add(key);
return Task.FromResult(key == "key-first"
? Sse("data: [DONE]\n\n")
: Sse("data: {\"choices\":[{\"delta\":{\"content\":\"исправлено\"}}]}\n\ndata: [DONE]\n\n"));
});
var settings = new AppSettingsService
{
Settings = new AppSettings
{
Ai = new AiSettings
{
FreeTierOnly = true,
ProviderOrder = ["groq"],
Connections =
[
new AiConnectionSettings
{
Id = "first",
ProviderId = "groq",
DisplayName = "First",
CredentialTarget = "AiteBar/AI/first"
},
new AiConnectionSettings
{
Id = "second",
ProviderId = "groq",
DisplayName = "Second",
CredentialTarget = "AiteBar/AI/second"
}
]
}
}
};
var gateway = new AiGateway(
settings,
new AiProviderClient(new HttpClient(handler), credentials),
TimeProvider.System);

AiGatewayStream stream = await gateway.GenerateTextProcessingStreamingAsync(Request());

Assert.Equal("second", stream.ConnectionId);
Assert.Equal("исправлено", await CollectAsync(stream.Chunks));
Assert.Equal(["key-first", "key-second"], generationAttempts);
Assert.Equal(
AiConnectionState.Unavailable,
gateway.GetQuotaStatus(settings.Settings.Ai.Connections[0], "llama-3.3-70b-versatile")?.State);
}

[Fact]
public async Task StreamRead_ThrowsTimeoutAfterConfiguredInactivity()
{
Expand Down
19 changes: 18 additions & 1 deletion AiteBar.Tests/AppSettingsLayoutContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public sealed class AppSettingsLayoutContractTests
"SliderActivationZone", "LblActivationZone10", "LblActivationZone30", "LblActivationZone50", "LblActivationZone100",
"SliderActivationDelay", "LblActivationDelay100", "LblActivationDelay200", "LblActivationDelay300", "LblActivationDelay500",
"TxtAboutVersion", "SettingsFooter", "BtnKeepOnTop", "AiConnectionsList", "TxtAiConnectionsEmpty",
"ChkShowTaskbarPositionIndicator", "ChkSecondaryMonitor", "ChkCheckForUpdatesEnabled",
"ChkShowPanelOnMouseHover", "ChkShowTaskbarPositionIndicator", "ChkSecondaryMonitor", "ChkCheckForUpdatesEnabled",
"PanelContextsList",
"HotkeyShowPanel", "HotkeyNextContext", "HotkeyPreviousContext", "HotkeyAddButton",
"HotkeyFileSorter", "HotkeyIconConverter", "HotkeyQuickNote", "HotkeyColorPicker", "HotkeyTimerStopwatch", "HotkeyQRCodeGenerator", "HotkeyClipboardManager",
Expand Down Expand Up @@ -61,6 +61,23 @@ public void ExistingSettingsHandlers_RemainWired()
AssertHandlerCount(window, "BtnSave_Click", 1);
}

[Fact]
public void MouseHoverSwitch_AppearsImmediatelyBeforePanelIndicatorSwitch()
{
XDocument window = LoadWindow();
XElement hoverSwitch = FindNamedElement(window, "ChkShowPanelOnMouseHover");
XElement indicatorSwitch = FindNamedElement(window, "ChkShowTaskbarPositionIndicator");
XElement settingsCard = Assert.IsType<XElement>(hoverSwitch.Parent?.Parent?.Parent);
XElement[] namedSwitches = settingsCard
.Descendants(PresentationNamespace + "CheckBox")
.Where(element => element.Attribute(XamlNamespace + "Name") != null)
.ToArray();

int hoverIndex = Array.IndexOf(namedSwitches, hoverSwitch);
int indicatorIndex = Array.IndexOf(namedSwitches, indicatorSwitch);
Assert.Equal(hoverIndex + 1, indicatorIndex);
}

[Fact]
public void ModernControls_ReplaceLegacyEditorsAndPreserveStagedSwitches()
{
Expand Down
25 changes: 25 additions & 0 deletions AiteBar.Tests/AppSettingsServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,29 @@
}
}

[Fact]
public async Task LoadAsync_LegacySettingsWithoutMouseHoverOption_PreservesHoverActivation()
{
string root = Path.Combine(Path.GetTempPath(), "AiteBarTests", Guid.NewGuid().ToString("N"));

Check notice

Code scanning / CodeQL

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note test

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments.
Directory.CreateDirectory(root);
string settingsPath = Path.Combine(root, "settings.json");

Check notice

Code scanning / CodeQL

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note test

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments.
string configPath = Path.Combine(root, "custom_buttons.json");

Check notice

Code scanning / CodeQL

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note test

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments.

try
{
await File.WriteAllTextAsync(settingsPath, "{}");
var service = new AppSettingsService(configPath, settingsPath);

await service.LoadAsync();

Assert.True(service.Settings.ShowPanelOnMouseHover);
}
finally
{
Directory.Delete(root, recursive: true);
}
}

[Fact]
public void GetBackupFilePath_ReturnsCorrectPath()
{
Expand Down Expand Up @@ -1004,6 +1027,7 @@
UiCulture = "de",
ActiveContextId = "context-3",
CheckForUpdatesEnabled = false,
ShowPanelOnMouseHover = false,
ShowTaskbarPositionIndicator = false,
TaskbarIndicatorPositionX = 0.75,
TaskbarIndicatorPositionY = 0.25,
Expand Down Expand Up @@ -1132,6 +1156,7 @@
Assert.Equal("de", clone.UiCulture);
Assert.Equal("context-3", clone.ActiveContextId);
Assert.False(clone.CheckForUpdatesEnabled);
Assert.False(clone.ShowPanelOnMouseHover);
Assert.False(clone.ShowTaskbarPositionIndicator);
Assert.Equal(0.75, clone.TaskbarIndicatorPositionX);
Assert.Equal(0.25, clone.TaskbarIndicatorPositionY);
Expand Down
30 changes: 30 additions & 0 deletions AiteBar.Tests/CommandButtonStyleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ await RunStaAsync(() =>
[Theory]
[InlineData("TimerStopwatchWindow.xaml", "name", "BtnStartPause", "PrimaryCommandButtonStyle")]
[InlineData("TimerStopwatchWindow.xaml", "name", "BtnReset", "CommandButtonStyle")]
[InlineData("FileSorterWindow.xaml", "name", "BtnAddFolder", "CommandButtonStyle")]
[InlineData("FileSorterWindow.xaml", "name", "BtnSort", "PrimaryCommandButtonStyle")]
[InlineData("IconConverterWindow.xaml", "click", "BtnChoose_Click", "CommandButtonStyle")]
[InlineData("IconConverterWindow.xaml", "name", "BtnSave", "PrimaryCommandButtonStyle")]
Expand Down Expand Up @@ -158,6 +159,35 @@ public void TimerCompactButtons_ResetInheritedCommandPaddingSoGlyphsRemainVisibl
}
}

[Fact]
public void MainPanelButtons_HaveVisibleKeyboardFocusGeometry()
{
XDocument window = LoadXaml("MainWindow.xaml");
XElement[] focusChromeBorders = window
.Descendants(PresentationNamespace + "Border")
.Where(element => string.Equals(
element.Attribute(XamlNamespace + "Name")?.Value,
"FocusChrome",
StringComparison.Ordinal))
.ToArray();

Assert.Equal(2, focusChromeBorders.Length);
Assert.All(focusChromeBorders, border =>
{
Assert.Equal("1", border.Attribute("BorderThickness")?.Value);
Assert.Equal("Transparent", border.Attribute("BorderBrush")?.Value);
});

Assert.Equal(2, window.Descendants(PresentationNamespace + "MultiTrigger").Count(trigger =>
trigger.Descendants(PresentationNamespace + "Condition").Any(condition =>
condition.Attribute("Property")?.Value == "local:KeyboardFocusVisualService.ShowKeyboardFocusCue" &&
condition.Attribute("Value")?.Value == "True") &&
trigger.Descendants(PresentationNamespace + "Setter").Any(setter =>
setter.Attribute("TargetName")?.Value == "FocusChrome" &&
setter.Attribute("Property")?.Value == "BorderBrush" &&
setter.Attribute("Value")?.Value == "#3ABEFF")));
}

[Fact]
public async Task TimerCompactButtonTemplate_LeavesMeasuredSpaceForGlyph()
{
Expand Down
Loading