diff --git a/AI_HANDOFF.md b/AI_HANDOFF.md
index 0874e83..1761729 100644
--- a/AI_HANDOFF.md
+++ b/AI_HANDOFF.md
@@ -1,5 +1,31 @@
# AI handoff
+## 2026-08-13 GitHub upload (complete)
+
+- User authorized uploading the complete current verified worktree, including the WPF redesign and installer-experience hardening. Scope is this repository only.
+- Remote audit after `fetch --prune --tags` found local `main` and `origin/main` identical with no other pre-existing branches. Initial source commit `96da297` was pushed to `agent/ui-installer-experience`; Draft PR #6 targets `main`: https://github.com/nojackno2-ctrl/ThermoTray/pull/6
+- GitHub connector PR creation returned `403 Resource not accessible by integration`; the authenticated host `gh pr create` fallback succeeded. A final handoff-only commit records the completed remote state on the same PR branch.
+- This authorization does not include a GitHub Release, tag, installer execution, or installed-application update.
+
+## 2026-08-13 installer experience hardening (complete, uncommitted)
+
+- Scope is limited to `installer/ThermoTray.iss` and a new installer contract test; the concurrent uncommitted WPF redesign remains preserved and untouched.
+- The installer now always creates Start Menu application/uninstall shortcuts with a stable AppUserModelID and `{app}` working directory; the desktop shortcut remains optional and defaults unchecked.
+- Added previous-install preference reuse, setup/uninstall logging, explicit uninstall identity/version fields, GitHub support/update metadata, installed README/LICENSE files, and matching bilingual README guidance for manual Start/Taskbar pinning. The existing AppMutex handover and `CloseApplications=no` safety design are preserved.
+- First test compile failed only because this test project does not enable implicit `System`/`System.IO` imports and the new standalone test omitted them. The imports were added before the successful rerun; no product or installer failure was observed.
+- Verification after correction: Release build passed with 0 warnings/errors, tests passed 152/152, formatting passed, self-contained publish passed, and `mt.exe` verified the embedded `requireAdministrator` manifest. Inno Setup 6.7.3 compiled the installer and the canonical release packaging regenerated `SHA256SUMS.txt`. Final installer: `artifacts/release/ThermoTray-Setup-1.1.7.exe`, 54,036,016 bytes, SHA-256 `920018D3F19E4CEAD2B617079D7CF2B2BD0F1C9275CADA4A399DBC313E862686`, file version 1.1.7.
+- No installer was executed, no installed application was updated, and no commit/push/release was performed.
+
+## 2026-08-13 taste-skill UI redesign
+
+- Scope is a UI-only targeted evolution of the elevated WPF monitor. The sensor pipeline, availability rules, multi-GPU identity, polling cadence, tray rendering, and persisted per-metric tray visibility are explicitly preserved.
+- Design Read: a trustworthy, compact Windows hardware instrument panel for everyday users and diagnostics. Dials are `DESIGN_VARIANCE 4`, `MOTION_INTENSITY 2`, and `VISUAL_DENSITY 8`; native WPF remains the only design system and no dependency is being added.
+- Existing audit: the cool dark palette used separate mint CPU and blue GPU accents, generic rounded cards, cramped two-column CPU/GPU composition, repeated tiny tray toggles, a visually weak status line, mixed corner radii, and no explicit high-contrast palette. Information architecture is sound and remains header, live devices, contextual status/action, and preferences.
+- Implemented one restrained mint accent, neutral device identity, full-width device rows with paired usage/temperature metrics, consistent 12 px panel and 8 px control radii, clearer focus/hover/pressed/disabled states, scroll-safe multi-GPU layout, contextual status panel, and runtime high-contrast resource switching. Empty status text collapses the status panel; actionable/error states remain contextual and use polite UI Automation live announcements.
+- `MainWindow.xaml` keeps every existing binding and tray visibility toggle. It caps the live-device surface at 560 px with vertical scrolling, so a second or later GPU no longer makes the whole window exceed the work area. Numeric readings use the installed Consolas monospace face while interface copy uses Segoe UI Variable Text/Segoe UI. No package or font dependency was added.
+- Added `StringToVisibilityConverter` and expanded the rendered-window layout contract with scroll-safety, dense minimum-width, single-accent, and radius assertions. Release build passed with 0 warnings/errors; all 150 tests passed; `dotnet format --verify-no-changes` and `git diff --check` passed. The first sandboxed build was blocked from the user's NuGet config; the host build then exposed ambiguous `SystemColors` because WinForms and WPF are both enabled, which was fixed with explicit `System.Windows.SystemColors` references before the clean build.
+- Visual scope is verified through the existing STA rendered-window tests, not by an inspected desktop screenshot. The shipped app requires elevation, and no UAC/elevated live launch was performed, so actual high-contrast switching, live hardware values, tray visuals/order, and desktop-scale appearance remain unverified in this pass.
+
## 2026-08-12 v1.1.7 publication and installed update
- Published GitHub Release `v1.1.7` from `e222bdea567951b2e5df7a0710f08ee1f02b2d66`; branch Build and tag Release succeeded. Downloaded digests: ZIP `B36D4FB1...91E11EC`, Setup `70A42B87...5331A`.
@@ -349,4 +375,12 @@ Findings and what each one changed. None of them reproduced on this machine's ow
- Corrected pre-commit assets: portable ZIP contains 8 expected files and no stale running executable; ZIP SHA-256 `47F5653BE7CAE19BB9E0800591A440420665186707A2916FE0ADE2BA4A8B5DEB`; Setup SHA-256 `6823B36DA9665E25365B12E97612F7C3B9DAC5A9307D29E083D94AB9FB57361C`.
- Notification-area ordering and fresh elevated hardware/UI behavior were not revalidated in this pass; prior v1.1.5 hardware evidence remains the latest live proof.
+## 2026-08-13 installed software update verification
+
+- Silently updated the current-user installation with `artifacts\release\ThermoTray-Setup-1.1.7.exe`; the installer returned exit code `0` and did not request a reboot.
+- Installed executable: `%LOCALAPPDATA%\Programs\ThermoTray\ThermoTray.exe`; FileVersion `1.1.7.0`, ProductVersion `1.1.7+acef9fb0781f2d6a7f9a4c995aba7843efad56a8`, SHA-256 `9ED1C1773A93415184B07772FFCB1CC025A4A51F6D9EEE6668A047645B663D91`. It is byte-for-byte identical to the current publish output.
+- Verified the registered version, install location, uninstall command, project/issues/update URLs, and the Start Menu launch/uninstall shortcuts. Removed the stale duplicate root-level `ThermoTray.lnk`; the current program-group shortcut remains valid.
+- Launched the exact installed executable with elevation; PID `39000` reported `Responding=True`. This verifies elevated launch and process responsiveness only; sensor correctness and fresh visual behavior were not revalidated.
+- The user's authorization in this milestone covers updating installed software; this handoff entry is not yet committed or pushed.
+
diff --git a/README.md b/README.md
index 30c4747..c4b1807 100644
--- a/README.md
+++ b/README.md
@@ -79,7 +79,7 @@ PawnIO 基於安全性僅允許已提權(Elevated)的處理程序存取核
| 檔案類型 | 檔案名稱 | 說明 |
| :--- | :--- | :--- |
-| **安裝版** | `ThermoTray-Setup-x.x.x.exe` | Inno Setup 安裝精靈,安裝至使用者的 LocalAppData 目錄,支援覆蓋升級與自動偵測執行狀態。 |
+| **安裝版** | `ThermoTray-Setup-x.x.x.exe` | Inno Setup 安裝精靈,安裝至使用者的 LocalAppData,支援覆蓋升級與執行狀態偵測;固定建立開始功能表的啟動/解除安裝捷徑,可自行釘選到開始或工作列,桌面捷徑則為預設不勾選的選用項目。 |
| **可攜版** | `ThermoTray-x.x.x-win-x64-portable.zip` | 免安裝綠色壓縮檔,解壓縮至任意目錄即可直接執行 `ThermoTray.exe`。 |
---
@@ -231,7 +231,7 @@ PawnIO requires elevated process privileges to access hardware registers.
Visit the [GitHub Releases Page](https://github.com/nojackno2-ctrl/ThermoTray/releases/latest):
-- **Setup Installer (`ThermoTray-Setup-x.x.x.exe`)**: Installs to `%LOCALAPPDATA%`, checks for running instances, and supports clean upgrades.
+- **Setup Installer (`ThermoTray-Setup-x.x.x.exe`)**: Installs to `%LOCALAPPDATA%`, checks for running instances, supports clean upgrades, and always creates Start Menu launch/uninstall shortcuts that users can pin manually. The optional desktop shortcut is off by default.
- **Portable ZIP (`ThermoTray-x.x.x-win-x64-portable.zip`)**: Extract anywhere and launch `ThermoTray.exe`.
---
diff --git a/installer/ThermoTray.iss b/installer/ThermoTray.iss
index 750aea2..42a138b 100644
--- a/installer/ThermoTray.iss
+++ b/installer/ThermoTray.iss
@@ -11,16 +11,30 @@
#define AppVersion "1.1.7"
#endif
#define AppPublisher "ThermoTray"
+#define AppURL "https://github.com/nojackno2-ctrl/ThermoTray"
#define AppExeName "ThermoTray.exe"
+#define AppUserModelId "nojackno2.ThermoTray"
[Setup]
AppId={{1B57D245-2F65-4E07-B338-CC94F7C7E6CB}
AppName={#AppName}
AppVersion={#AppVersion}
+AppVerName={#AppName} v{#AppVersion}
AppPublisher={#AppPublisher}
+AppPublisherURL={#AppURL}
+AppSupportURL={#AppURL}/issues
+AppUpdatesURL={#AppURL}/releases
+AppReadmeFile={app}\README.md
+VersionInfoVersion={#AppVersion}.0
+VersionInfoCompany={#AppPublisher}
+VersionInfoDescription={#AppName} installer
+VersionInfoTextVersion={#AppVersion}
+VersionInfoProductVersion={#AppVersion}.0
+VersionInfoProductName={#AppName}
; 安裝至使用者 AppData\Local 目錄,不需系統管理員安裝權限
DefaultDirName={localappdata}\Programs\{#AppName}
DefaultGroupName={#AppName}
+AllowNoIcons=no
DisableProgramGroupPage=yes
OutputDir=..\artifacts\installer
OutputBaseFilename=ThermoTray-Setup-{#AppVersion}
@@ -30,10 +44,19 @@ ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
PrivilegesRequired=lowest
UninstallDisplayIcon={app}\{#AppExeName}
+UninstallDisplayName={#AppName} v{#AppVersion}
+Uninstallable=yes
+UsePreviousAppDir=yes
+UsePreviousGroup=yes
+UsePreviousTasks=yes
+CreateUninstallRegKey=yes
+SetupLogging=yes
+UninstallLogging=yes
; 檢查應用程式產生的全域互斥鎖,避免在 ThermoTray 執行中進行覆蓋安裝
AppMutex=Global\ThermoTray.Setup
; 關閉預設的 Restart Manager,改由 AppMutex 提示使用者結束程式
CloseApplications=no
+RestartApplications=no
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
@@ -41,13 +64,16 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
; 包含發布目錄下的所有檔案(排除 .pdb 偵錯檔)
Source: "..\publish\win-x64\*"; DestDir: "{app}"; Excludes: "*.pdb"; Flags: ignoreversion recursesubdirs createallsubdirs
+Source: "..\README.md"; DestDir: "{app}"; Flags: ignoreversion
+Source: "..\LICENSE"; DestDir: "{app}"; Flags: ignoreversion
[Tasks]
Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional shortcuts:"; Flags: unchecked
[Icons]
-Name: "{autoprograms}\{#AppName}"; Filename: "{app}\{#AppExeName}"
-Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon
+Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; WorkingDir: "{app}"; AppUserModelID: "{#AppUserModelId}"
+Name: "{group}\{cm:UninstallProgram,{#AppName}}"; Filename: "{uninstallexe}"
+Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; WorkingDir: "{app}"; AppUserModelID: "{#AppUserModelId}"; Tasks: desktopicon
[Registry]
; 舊版本 Run 登錄項清理標記
diff --git a/src/ThermoTray/App.xaml b/src/ThermoTray/App.xaml
index 6170ef8..ec7d71b 100644
--- a/src/ThermoTray/App.xaml
+++ b/src/ThermoTray/App.xaml
@@ -1,69 +1,127 @@
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:local="clr-namespace:ThermoTray">
-
-
-
-
-
-
-
-
-
-
+
+
+
+ #171A1F
+ #1D2229
+ #232A33
+ #2B343F
+ #313C49
+ #3D4856
+ #45C9A8
+ #171A1F
+ #F3F6F8
+ #B7C1CC
+ #8E9BAA
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ThermoTray/MainWindow.xaml b/src/ThermoTray/MainWindow.xaml
index 0615003..0edf136 100644
--- a/src/ThermoTray/MainWindow.xaml
+++ b/src/ThermoTray/MainWindow.xaml
@@ -2,131 +2,308 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:primitives="clr-namespace:System.Windows.Controls.Primitives;assembly=PresentationFramework"
- Title="ThermoTray" Width="540" MinWidth="460"
- Background="{StaticResource PanelBrush}" Foreground="White"
+ Title="ThermoTray"
+ Width="720"
+ MinWidth="600"
+ MaxHeight="{Binding Source={x:Static SystemParameters.WorkArea}, Path=Height}"
+ SizeToContent="Height"
+ Background="{DynamicResource WindowBrush}"
+ Foreground="{DynamicResource TextPrimaryBrush}"
+ FontFamily="Segoe UI Variable Text, Segoe UI"
+ FontSize="14"
Icon="ThermoTray.ico"
WindowStartupLocation="CenterScreen"
- SizeToContent="Height">
-
-
+ UseLayoutRounding="True"
+ SnapsToDevicePixels="True"
+ KeyboardNavigation.TabNavigation="Cycle">
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 繁體中文
- English
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 繁體中文
+ English
+
+
diff --git a/src/ThermoTray/MainWindow.xaml.cs b/src/ThermoTray/MainWindow.xaml.cs
index 2e5dc0e..f159462 100644
--- a/src/ThermoTray/MainWindow.xaml.cs
+++ b/src/ThermoTray/MainWindow.xaml.cs
@@ -1,6 +1,7 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
+using System.Windows.Media;
using System.Windows.Navigation;
namespace ThermoTray;
@@ -10,6 +11,23 @@ namespace ThermoTray;
///
public partial class MainWindow : Window
{
+ private static readonly string[] PaletteResourceKeys =
+ [
+ "WindowBrush",
+ "PanelBrush",
+ "SurfaceBrush",
+ "SurfaceHoverBrush",
+ "SurfacePressedBrush",
+ "BorderBrush",
+ "AccentBrush",
+ "AccentTextBrush",
+ "TextPrimaryBrush",
+ "TextSecondaryBrush",
+ "TextMutedBrush",
+ ];
+
+ private readonly Dictionary _standardPalette = new(StringComparer.Ordinal);
+
///
/// 標記是否為真正的關閉請求(由選單「結束」觸發),而非點擊右上角 X 按鈕隱藏至系統匣。
///
@@ -21,7 +39,62 @@ public partial class MainWindow : Window
public MainWindow()
{
InitializeComponent();
+ CaptureStandardPalette();
+ ApplyAccessibilityPalette();
+ SystemParameters.StaticPropertyChanged += OnSystemParametersStaticPropertyChanged;
StateChanged += OnStateChanged;
+ Closed += OnClosed;
+ }
+
+ private void CaptureStandardPalette()
+ {
+ foreach (var key in PaletteResourceKeys)
+ {
+ if (System.Windows.Application.Current.Resources[key] is object value)
+ {
+ _standardPalette[key] = value;
+ }
+ }
+ }
+
+ private void ApplyAccessibilityPalette()
+ {
+ var resources = System.Windows.Application.Current.Resources;
+ if (!SystemParameters.HighContrast)
+ {
+ foreach (var pair in _standardPalette)
+ {
+ resources[pair.Key] = pair.Value;
+ }
+
+ return;
+ }
+
+ resources["WindowBrush"] = System.Windows.SystemColors.WindowBrush;
+ resources["PanelBrush"] = System.Windows.SystemColors.WindowBrush;
+ resources["SurfaceBrush"] = System.Windows.SystemColors.ControlBrush;
+ resources["SurfaceHoverBrush"] = System.Windows.SystemColors.ControlLightBrush;
+ resources["SurfacePressedBrush"] = System.Windows.SystemColors.ControlDarkBrush;
+ resources["BorderBrush"] = System.Windows.SystemColors.ActiveBorderBrush;
+ resources["AccentBrush"] = System.Windows.SystemColors.HighlightBrush;
+ resources["AccentTextBrush"] = System.Windows.SystemColors.HighlightTextBrush;
+ resources["TextPrimaryBrush"] = System.Windows.SystemColors.WindowTextBrush;
+ resources["TextSecondaryBrush"] = System.Windows.SystemColors.WindowTextBrush;
+ resources["TextMutedBrush"] = System.Windows.SystemColors.GrayTextBrush;
+ }
+
+ private void OnSystemParametersStaticPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (string.Equals(e.PropertyName, nameof(SystemParameters.HighContrast), StringComparison.Ordinal))
+ {
+ ApplyAccessibilityPalette();
+ }
+ }
+
+ private void OnClosed(object? sender, EventArgs e)
+ {
+ SystemParameters.StaticPropertyChanged -= OnSystemParametersStaticPropertyChanged;
+ Closed -= OnClosed;
}
///
diff --git a/src/ThermoTray/StringToVisibilityConverter.cs b/src/ThermoTray/StringToVisibilityConverter.cs
new file mode 100644
index 0000000..4dc41f0
--- /dev/null
+++ b/src/ThermoTray/StringToVisibilityConverter.cs
@@ -0,0 +1,18 @@
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace ThermoTray;
+
+///
+/// Collapses contextual UI when its bound message is empty, while keeping non-empty status and
+/// error states in the accessibility tree.
+///
+public sealed class StringToVisibilityConverter : IValueConverter
+{
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
+ string.IsNullOrWhiteSpace(value as string) ? Visibility.Collapsed : Visibility.Visible;
+
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
+ throw new NotSupportedException();
+}
diff --git a/tests/ThermoTray.Tests/InstallerContractTests.cs b/tests/ThermoTray.Tests/InstallerContractTests.cs
new file mode 100644
index 0000000..564af9d
--- /dev/null
+++ b/tests/ThermoTray.Tests/InstallerContractTests.cs
@@ -0,0 +1,81 @@
+using System;
+using System.IO;
+using Xunit;
+
+namespace ThermoTray.Tests;
+
+///
+/// 驗證安裝器持續提供一致的開始功能表、升級、解除安裝與診斷體驗。
+///
+public sealed class InstallerContractTests
+{
+ ///
+ /// 驗證升級設定、應用程式與功能中繼資料,以及安裝紀錄不會被後續修改移除。
+ ///
+ [Fact]
+ public void InstallerProvidesConsistentUpgradeAndDiagnosticsMetadata()
+ {
+ var script = ReadInstallerScript();
+
+ foreach (var directive in new[]
+ {
+ "AllowNoIcons=no",
+ "DisableProgramGroupPage=yes",
+ "UsePreviousAppDir=yes",
+ "UsePreviousGroup=yes",
+ "UsePreviousTasks=yes",
+ "CreateUninstallRegKey=yes",
+ "SetupLogging=yes",
+ "UninstallLogging=yes",
+ "CloseApplications=no",
+ "RestartApplications=no"
+ })
+ {
+ Assert.Contains(directive, script.Lines, StringComparer.Ordinal);
+ }
+
+ Assert.Contains("AppPublisherURL={#AppURL}", script.Lines, StringComparer.Ordinal);
+ Assert.Contains("AppSupportURL={#AppURL}/issues", script.Lines, StringComparer.Ordinal);
+ Assert.Contains("AppUpdatesURL={#AppURL}/releases", script.Lines, StringComparer.Ordinal);
+ }
+
+ ///
+ /// 驗證開始功能表固定建立啟動與解除安裝捷徑,而桌面捷徑仍由使用者自行選擇。
+ ///
+ [Fact]
+ public void StartMenuAndOptionalShortcutPolicyIsPreserved()
+ {
+ var script = ReadInstallerScript();
+ var startMenuShortcut = Assert.Single(script.Lines, line =>
+ line.StartsWith("Name: \"{group}\\{#AppName}\";", StringComparison.Ordinal));
+ var uninstallShortcut = Assert.Single(script.Lines, line =>
+ line.Contains("{cm:UninstallProgram,{#AppName}}", StringComparison.Ordinal));
+ var desktopTask = Assert.Single(script.Lines, line =>
+ line.StartsWith("Name: \"desktopicon\";", StringComparison.Ordinal));
+
+ Assert.DoesNotContain("Tasks:", startMenuShortcut, StringComparison.Ordinal);
+ Assert.DoesNotContain("Tasks:", uninstallShortcut, StringComparison.Ordinal);
+ Assert.Contains("WorkingDir: \"{app}\"", startMenuShortcut, StringComparison.Ordinal);
+ Assert.Contains("AppUserModelID:", startMenuShortcut, StringComparison.Ordinal);
+ Assert.Contains("Flags: unchecked", desktopTask, StringComparison.Ordinal);
+ }
+
+ private static (string Content, string[] Lines) ReadInstallerScript()
+ {
+ var repositoryRoot = FindRepositoryRoot();
+ var content = File.ReadAllText(Path.Combine(repositoryRoot, "installer", "ThermoTray.iss"));
+ return (content, content.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries));
+ }
+
+ private static string FindRepositoryRoot()
+ {
+ var directory = new DirectoryInfo(AppContext.BaseDirectory);
+ while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "ThermoTray.sln")))
+ {
+ directory = directory.Parent;
+ }
+
+ Assert.NotNull(directory);
+ return directory.FullName;
+ }
+}
diff --git a/tests/ThermoTray.Tests/MainWindowLayoutTests.cs b/tests/ThermoTray.Tests/MainWindowLayoutTests.cs
index 2c1142a..05a15bb 100644
--- a/tests/ThermoTray.Tests/MainWindowLayoutTests.cs
+++ b/tests/ThermoTray.Tests/MainWindowLayoutTests.cs
@@ -24,7 +24,8 @@ public sealed class MainWindowLayoutTests : IClassFixture
public void TheCardsShowTheirWholeContents() =>
_fixture.Invoke(window =>
{
- AssertContentFits(window.CpuCard, "CPU");
+ Assert.True(window.CpuCard.ActualHeight > 0, "the CPU panel renders nothing");
+ Assert.True(window.CpuCard.DesiredSize.Height <= window.CpuCard.ActualHeight + 0.5);
Assert.Equal(2, window.GpuCards.Items.Count);
Assert.True(window.GpuCards.ActualHeight > 0, "the independent GPU cards render nothing");
});
@@ -33,11 +34,9 @@ public void TheCardsShowTheirWholeContents() =>
public void TheCpuDeviceNameIsTheFirstCardLineAndEachMetricHasATrayToggle() =>
_fixture.Invoke(window =>
{
- var stack = (StackPanel)window.CpuCard.Child;
-
- Assert.Equal("AMD Ryzen 9 5900HS with Radeon Graphics", ((TextBlock)stack.Children[0]).Text);
- Assert.IsType(((Grid)stack.Children[1]).Children[1]);
- Assert.IsType(((Grid)stack.Children[3]).Children[1]);
+ Assert.Equal("AMD Ryzen 9 5900HS with Radeon Graphics", window.CpuDeviceNameText.Text);
+ Assert.True(window.CpuUsageTrayToggle.IsChecked);
+ Assert.True(window.CpuTemperatureTrayToggle.IsChecked);
});
///
@@ -48,7 +47,7 @@ public void TheSettingsBelowTheCardsStayInTheWindow() =>
_fixture.Invoke(window =>
{
var content = (FrameworkElement)window.Content;
- var settings = (FrameworkElement)((Grid)content).Children[^1];
+ var settings = window.SettingsPanel;
var bottom = settings.TransformToAncestor(content).TransformBounds(new Rect(settings.RenderSize)).Bottom;
Assert.True(
@@ -71,8 +70,7 @@ public void TheHeaderShowsTheProductVersion() =>
_fixture.Invoke(window =>
{
var content = (FrameworkElement)window.Content;
- var title = (StackPanel)((StackPanel)((Grid)content).Children[0]).Children[0];
- var version = (TextBlock)title.Children[1];
+ var version = window.ProductVersionText;
Assert.Equal(MainViewModel.FormatVersion(typeof(MainViewModel).Assembly.GetName().Version), version.Text);
Assert.True(version.ActualWidth > 0, "the version renders nothing");
@@ -95,6 +93,49 @@ private static void AssertContentFits(Border card, string which)
bottom <= available + 0.5,
$"the {which} card cuts its last line off: it reaches {bottom} inside a card that ends at {available}");
}
+
+ [Fact]
+ public void TheMultiGpuSurfaceIsScrollSafeAndKeepsADenseMinimumWidth() =>
+ _fixture.Invoke(window =>
+ {
+ var scrollViewer = FindVisualChild(window.RootLayout);
+
+ Assert.NotNull(scrollViewer);
+ Assert.Equal(ScrollBarVisibility.Auto, scrollViewer.VerticalScrollBarVisibility);
+ Assert.Equal(560, scrollViewer.MaxHeight);
+ Assert.Equal(600, window.MinWidth);
+ });
+
+ [Fact]
+ public void TheRedesignUsesOneAccentAndConsistentPanelRadii() =>
+ _fixture.Invoke(window =>
+ {
+ Assert.True(_fixture.Application.Resources.Contains("AccentBrush"));
+ Assert.False(_fixture.Application.Resources.Contains("GpuAccentBrush"));
+ Assert.Equal(new CornerRadius(12), window.CpuCard.CornerRadius);
+ Assert.Equal(new CornerRadius(12), window.StatusPanel.CornerRadius);
+ });
+
+ private static T? FindVisualChild(DependencyObject parent)
+ where T : DependencyObject
+ {
+ for (var index = 0; index < System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent); index++)
+ {
+ var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, index);
+ if (child is T match)
+ {
+ return match;
+ }
+
+ var descendant = FindVisualChild(child);
+ if (descendant is not null)
+ {
+ return descendant;
+ }
+ }
+
+ return null;
+ }
}
///
@@ -158,6 +199,8 @@ public MainWindowFixture()
_application = application!;
}
+ public App Application => _application;
+
///
/// 在 UI 執行緒分派執行斷言。
///