hD}mUEIj1v~8LD
z4^b>U+!p;t?E88qAQeAgHCsCI`_-BhqQhhEam}wR0%Hk9yvhi-2~41Tlz)WhFcx*#
zBNHhK$ug~d=!Oe_8q`dgTP(D8Ag7F4u%4~<&s>kHFb(1wCRS{y#EX+MaWF&_z|1{$
zSTPa*Xp}L=HzO_R5gwIpcWNGbu)oaFzjMu}9OyjGzK{trvcBqNLBkpoA9p35KeEIB
z&7Dt8kEFbpvT2e3!fu|I1^%KqvqWiy1cTfOT2)LQj*kL(9p@VnKCh%hlVb}{)jaXzlg%p%+GHpgVygIVssjt7N8^U^L*oS=Dh)Ge!Y>-m&vBP25m;;tg3Q~<_LFh(@Sq)W;tGHT80
zp^^0^Y#99Qs{eWy?u7)CxyTOZjKL+qe?6-|GVL67=0*9Qp#C=fHhZz~rnKv;J|%2G
zOfBl#p^yZlP29b`_C^bK;imGYG>U_>d5p4IKhW8i*Wo9hDU~9OvM0|NTn453vie2m
zsY-<}@-yOs|Kx_2-av4-85vR5&v$yviVKXL=HRJNh+ac(Yt1zslPbI}MdJgDTTulW
z5wZ)6@4k#yw(B1il{29PO98?|SXP+@@E0ccZ%G?1qWj0GWK*qxVHi%K+rG+f8~Vvf
zC}Hj}nzHBvta{Wb@uF`vcfQM9x;UA!P9uXMq<3H7Ul8FJZ1(gn9?$=k$%s6U@?N`}
zd@58;VW-@eal%a1G{(2+%c_zkMZ~I4fKUPcs4e&8G_?TJ+@Rthb4vb*M0F3^I#SAqEwZP`%NZ8_
zN{u^)_tsx9Krmx>8T(}|T{taUL%N?SVy1LW_on0fR*OoL4@4`@g16fwO1_dDx-cWC
z9(j&*A?3F&2T2m3xN21Bz2mJulQW^AIZaeN}p72URajiG*%6eRjfYw
zm%2kBwsx^Bk(mRFiwA=@U)7nxuZ=VtCA+ruy0K;)wG
ztTeIn3T88_EEDtqHGYq(*K!7?4GxZ{V=(Ljk)H~eX5y@EY$^I%447A(slOY)U4t|0
z4s#V7^O}l>McwC2PHAZW#pUU6{M--%WMn}7!{x3XEdT^(G1V!B0#HP&(}wS@*&
z|F--C*+HCogu1*PyXA;&-%jMP&nr@?&qtXlhXGSqbYE+YP(E6$kYHL}sbMJr5S69j
z+iMQ@#A?l8|_)*F>
ze%Jio@j@0jc`KPLMDD<*zeZAk-c#TJ!&5=s^T;48wC6b0KXt$%4-On;Yg!!y{l8gQ
zm_0ktQ@v=!H2!R#_iGHmSbx|A61pk6QZ6cUe_0hTo%CK20mS2{_5|(~ni}QPNIV-WcZx4Vz;2CNLyKA9d%t0n?;g|mI3kE{h+^r9
zVC;Z$hlRI0{o-&$f1liy%YbP6OL1g@c0_p2SEKRV9@fJOsCu>b-6?RD`9dt%jQ+N8
zA~(bUP}bp0Bj;aAeRL4~H1`2PRo!#!n#TcbggK}guy$h99+2*-hJJVBHD|{iC@|`d
zK~#DhN!2SxW&gCLQ9fhtp*Fjw1)LH~h?pQc7vck<(5oc!CNTH-zWfp9yzn%f>RkHYr;EP~!MYKXtCF7>b%^o_73Z%
ztrdJZTOib%X2S%4*u6aQ78tH(URd2J!zk3ZBgh{+XP*rPk1UOF7pa!`LIWj&DVPTF3R$d6H7LS5~R*=Ho7tvnhJqyS<$VxVMbn0Nk>j0G}ZB1IbS!O0a{cZwQfE3iKVPom^`A$pTOb_$AUGNouYW
zJN_wvuE`ERNlyh_S(38TH2z`(IHXm{yjX+cwhYJ9dij$E%KtDiWS!{X7d^n%xSz%x
zpDR83JcKSIdXMDeqYbU4(o3pNi=Csezhd|np7QX_t7D}K<-N8iDp<*iUa)!Vh&
zqyJ3Jn3!8P7C49>W6zH7P-|2k?4qLYfS?Lhmmqb49GYCbNBDu3^p5DKpW6lKJ0f-B
zsmTaZMZ?C
z%kd0-M-=g_ATCOyBA3v>#)^)6k~MiecBJ6KfvDbq4jX
z-VBoJa%7=Lz~8-zp*^d%l`Ci(m2X=9CS1=YL=?~y7yIZ!4S6g)jde@IIb3dwf`JaP
zepJ&kx1jGI5jZsDs(9ier6m?88wnM$`iy5yi0({3vJ5mqiURgdCzzEPwNO`WiX2K(
ze*Z8PSTgtOsj1TIYSH3@v#WIO(7N7}y69lOjq?=+DIcwPT2H9B)^LTVJ`GShwcb-?
mj}oQX_y4vxsr6tG4Y}7$vaTwXDy1}Z!F62|9h{a^)c*h_Cshgn
literal 0
HcmV?d00001
diff --git a/tests/FeatureChecks.cs b/tests/FeatureChecks.cs
new file mode 100644
index 0000000..e550c96
--- /dev/null
+++ b/tests/FeatureChecks.cs
@@ -0,0 +1,61 @@
+using System.IO;
+using System.Reflection;
+using System.Text.Json;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using ShortcutsPanel;
+
+internal static class FeatureChecks
+{
+ static void Check(bool condition, string name) { if (!condition) throw new Exception(name); Console.WriteLine("PASS " + name); }
+ [STAThread]
+ static void Main()
+ {
+ var directory = Path.Combine(Path.GetTempPath(), "OrbitFeatures-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(directory);
+ var store = new SettingsStore(@"Software\OrbitTests-" + Guid.NewGuid().ToString("N"), Path.Combine(directory, "absent.json"), directory);
+ var app = new App(); app.InitializeComponent();
+ Check(MainWindow.LoadAppIcon().PixelWidth > 0, "About icon loads from embedded resource");
+ var window = new MainWindow(store);
+ object? Call(string method, params object?[] args) => typeof(MainWindow).GetMethod(method, BindingFlags.NonPublic | BindingFlags.Instance)!.Invoke(window, args);
+ try
+ {
+ var original = LibraryOperations.Validate([new("A", "https://a.example", "One", "#A697FF"), new("B", "https://b.example", "Two", "#60D6B2"), new("C", "https://c.example", "One", "#60D6B2")]);
+ Check(original.Select(x => x.Id).Distinct().Count() == 3, "Legacy links receive unique identities");
+ var moved = LibraryOperations.MoveBefore(original, original[2], original[0]);
+ Check(moved.Select(x => x.Name).SequenceEqual(new[] { "C", "A", "B" }), "Card reorder preserves all entries");
+ Check(LibraryOperations.MoveAfter(original, original[0], original[2]).Select(x => x.Name).SequenceEqual(new[] { "B", "C", "A" }), "Dragging after last card reaches final position");
+ var order = LibraryOperations.MoveBefore(new[] { "One", "Two", "Empty" }, "Two", "One");
+ Check((bool)Call("Save", moved, order)!, "Save reordered library");
+ Check(store.LoadCollections().SequenceEqual(order), "Collection order and empty collection persist");
+ Check(JsonSerializer.Deserialize>(store.Load()!)![0].Name == "C", "Card order persists");
+ Check((bool)Call("DeleteShortcut", original[0])!, "Delete shortcut");
+ Check(JsonSerializer.Deserialize>(store.Load()!)!.Count == 2, "Deletion saved");
+ Call("UndoDelete");
+ Check(JsonSerializer.Deserialize>(store.Load()!)!.Select(x => x.Name).SequenceEqual(new[] { "C", "A", "B" }), "Undo restores original position");
+ var pngPath = Path.Combine(directory, "icon.png");
+ var bitmap = BitmapSource.Create(2, 2, 96, 96, PixelFormats.Bgra32, null, new byte[] { 0,0,255,255, 0,255,0,255, 255,0,0,255, 255,255,255,255 }, 8);
+ var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(bitmap)); using (var file = File.Create(pngPath)) encoder.Save(file);
+ var icon = CustomIcons.FromFile(pngPath); moved[0] = moved[0] with { Icon = icon };
+ var export = new LibraryExport(1, moved, order);
+ var roundTrip = LibraryOperations.ParseExport(JsonSerializer.Serialize(export));
+ Check(roundTrip.Links[0].Icon == icon && CustomIcons.Decode(roundTrip.Links[0].Icon!).PixelWidth > 0, "Custom icon survives export/import");
+ Check(roundTrip.Collections.SequenceEqual(order) && roundTrip.Links.Select(x => x.Id).SequenceEqual(moved.Select(x => x.Id)), "Export retains identities and order");
+ Check(LibraryOperations.Merge(moved, roundTrip.Links).Count == 3, "Merge skips duplicate shortcuts");
+ Check(LibraryOperations.ParseExport(JsonSerializer.Serialize(original)).Links.Count == 3, "Legacy array import supported");
+ foreach (var invalid in new[] { "{}", "{\"Version\":99,\"Links\":[]}", "[{\"Name\":\"Unsafe\",\"Url\":\"file:///C:/test\",\"Group\":\"One\",\"Color\":\"#FFFFFF\"}]" })
+ {
+ bool rejected = false; try { LibraryOperations.ParseExport(invalid); } catch { rejected = true; }
+ Check(rejected, "Invalid import rejected");
+ }
+ Check((bool)Call("Save", roundTrip.Links, roundTrip.Collections)!, "Custom-icon library saves");
+ Check(JsonSerializer.Deserialize>(store.Load()!)![0].Icon == icon, "Custom icon persists in JSON");
+ }
+ finally
+ {
+ window.Close(); app.Shutdown();
+ foreach (var file in Directory.GetFiles(directory)) File.Delete(file);
+ Directory.Delete(directory);
+ }
+ }
+}
diff --git a/tests/FeatureChecks.csproj b/tests/FeatureChecks.csproj
new file mode 100644
index 0000000..a650ee4
--- /dev/null
+++ b/tests/FeatureChecks.csproj
@@ -0,0 +1,4 @@
+
+ Exenet8.0-windowstrueenableenablefalse
+
+
diff --git a/tests/Program.cs b/tests/Program.cs
new file mode 100644
index 0000000..1469d65
--- /dev/null
+++ b/tests/Program.cs
@@ -0,0 +1,41 @@
+using Microsoft.Win32;
+using ShortcutsPanel;
+var key = @"Software\OrbitStorageChecks\" + Guid.NewGuid().ToString("N");
+var folder = Path.Combine(Path.GetTempPath(), "OrbitStorageChecks-" + Guid.NewGuid().ToString("N"));
+Directory.CreateDirectory(folder);
+var legacy = Path.Combine(folder, "shortcuts.json");
+var dataFolder = Path.Combine(folder, "data");
+void Check(bool condition, string label) { if (!condition) throw new Exception(label); Console.WriteLine("PASS " + label); }
+SettingsStore Store() => new(key, legacy, dataFolder);
+try
+{
+ using (var old = Registry.CurrentUser.CreateSubKey(key)) { old.SetValue("Library", "[{\"Name\":\"Original\"}]"); old.SetValue("DarkTheme", 1); }
+ var store = Store();
+ Check(store.LoadDarkTheme(), "Registry theme migrated");
+ Check(store.Load()!.Contains("Original"), "Registry links migrated");
+ Check(File.Exists(store.SettingsPath), "Migration creates JSON in app data directory");
+ using (var old = Registry.CurrentUser.CreateSubKey(key)) old.SetValue("Library", "[]");
+ Check(Store().Load()!.Contains("Original"), "JSON takes precedence after migration");
+ store.Save("[{\"Name\":\"Edited\"}]");
+ Check(Store().Load()!.Contains("Edited"), "Edited links persist");
+ Check(Store().LoadDarkTheme(), "Link save preserves theme");
+ store.SaveDarkTheme(false);
+ Check(!Store().LoadDarkTheme() && Store().Load()!.Contains("Edited"), "Theme save preserves links");
+ Check(File.Exists(store.SettingsPath + ".bak"), "Previous JSON backed up");
+ store.Save("[]"); Check(Store().Load() == "[]", "Empty library persists");
+ File.WriteAllText(store.SettingsPath, "broken");
+ try { store.Save("[]"); throw new Exception("Corrupt settings overwritten"); } catch (System.Text.Json.JsonException) { }
+ Check(File.ReadAllText(store.SettingsPath) == "broken", "Corrupt settings preserved");
+ File.Delete(store.SettingsPath);
+ Registry.CurrentUser.DeleteSubKeyTree(key, false);
+ File.WriteAllText(legacy, "[]"); File.SetAttributes(legacy, FileAttributes.ReadOnly);
+ Check(Store().Load() == "[]", "Read-only sidecar migration");
+ Check(File.ReadAllText(legacy) == "[]", "Legacy file untouched");
+}
+finally
+{
+ Registry.CurrentUser.DeleteSubKeyTree(key, false);
+ if (File.Exists(legacy)) { File.SetAttributes(legacy, FileAttributes.Normal); File.Delete(legacy); }
+ if (Directory.Exists(dataFolder)) { foreach (var file in Directory.GetFiles(dataFolder)) File.Delete(file); Directory.Delete(dataFolder); }
+ Directory.Delete(folder);
+}
diff --git a/tests/StorageChecks.csproj b/tests/StorageChecks.csproj
new file mode 100644
index 0000000..a61662f
--- /dev/null
+++ b/tests/StorageChecks.csproj
@@ -0,0 +1,4 @@
+
+ Exenet8.0-windowsenableenable
+
+
diff --git a/tests/ThemeChecks.cs b/tests/ThemeChecks.cs
new file mode 100644
index 0000000..119269f
--- /dev/null
+++ b/tests/ThemeChecks.cs
@@ -0,0 +1,36 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.IO;
+using ShortcutsPanel;
+
+internal static class ThemeChecks
+{
+ [STAThread]
+ static void Main()
+ {
+ var discovered = WebsiteIcons.Discover("", new Uri("https://example.com/page")).ToArray();
+ if (discovered.Length != 2 || discovered[0].AbsoluteUri != "https://example.com/assets/logo.png?a=1&b=2" || discovered[1].AbsoluteUri != "https://example.com/favicon.ico") throw new Exception("Icon discovery failed");
+ Console.WriteLine("PASS icon discovery resolves relative URLs, entities, and filters unsupported icons");
+ foreach (var site in new[] { "https://github.com", "https://www.google.com" })
+ Console.WriteLine("Live icon " + site + ": " + (WebsiteIcons.Fetch(site).GetAwaiter().GetResult() != null ? "loaded" : "fallback"));
+ var app = new App(); app.InitializeComponent();
+ var window = new MainWindow();
+ var content = (FrameworkElement)window.Content;
+ var output = Path.Combine(Path.GetTempPath(), "OrbitThemePreview"); Directory.CreateDirectory(output);
+ foreach (var dark in new[] { false, true, false })
+ {
+ Theme.Apply(dark);
+ content.Measure(new Size(1100, 680)); content.Arrange(new Rect(0, 0, 1100, 680)); content.UpdateLayout();
+ if (((SolidColorBrush)window.Background).Color != (Color)ColorConverter.ConvertFromString(dark ? "#202020" : "#FAF9F6")) throw new Exception("Window theme did not update");
+ var bitmap = new RenderTargetBitmap(1100, 680, 96, 96, PixelFormats.Pbgra32);
+ var frame = new DrawingVisual(); using (var dc = frame.RenderOpen()) { dc.DrawRectangle(window.Background, null, new Rect(0,0,1100,680)); dc.DrawRectangle(new VisualBrush(content), null, new Rect(0,0,1100,680)); }
+ bitmap.Render(frame); var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(bitmap));
+ using var file = File.Create(Path.Combine(output, dark ? "dark.png" : "light.png")); encoder.Save(file);
+ Console.WriteLine("PASS rendered " + (dark ? "dark" : "light") + " theme");
+ }
+ Console.WriteLine(output);
+ window.Close(); app.Shutdown();
+ }
+}
diff --git a/tests/ThemeChecks.csproj b/tests/ThemeChecks.csproj
new file mode 100644
index 0000000..d039ad5
--- /dev/null
+++ b/tests/ThemeChecks.csproj
@@ -0,0 +1,4 @@
+
+ Exenet8.0-windowstrueenableenablefalse
+
+