using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Xml.Linq; using MmdPdf; using Xunit; namespace MmdPdf.Tests { /// /// Invariants of the one shortcut table (Services/ShortcutTable.cs). /// /// The table was unified in 1.7.5 from two hand maintained copies that had quietly drifted: /// Alt+M existed in the list and not on the keyboard map, Home and End were captioned /// differently in each view, and Ctrl+B was documented as bold in one section of the list and /// as the sidebar in another while the code did neither consistently. Unifying removes the /// opportunity for that; these tests are what keep it removed. /// /// No WPF here on purpose. The table is plain data, which is why the csproj links the single /// file rather than referencing the app, the same arrangement OcrCatalogTests uses. /// public class ShortcutTableTests { private static readonly string StringsDir = Path.Combine(RepoRoot(), "Strings"); private static string RepoRoot() { // Walk up from the test binary until the folder holding Strings\en-US.xaml appears. var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir != null && !File.Exists(Path.Combine(dir.FullName, "Strings", "en-US.xaml"))) dir = dir.Parent; Assert.True(dir != null, "could not locate the repo root from the test output folder"); return dir!.FullName; } private static HashSet EnglishKeys() { var doc = XDocument.Load(Path.Combine(StringsDir, "en-US.xaml")); XNamespace x = "http://schemas.microsoft.com/winfx/2006/xaml"; return doc.Root!.Elements() .Select(e => (string?)e.Attribute(x + "Key")) .Where(k => k != null) .Select(k => k!) .ToHashSet(); } /// /// THE one that matters. Two bindings claiming the same key on the same layer means the /// keyboard map silently shows whichever was declared last, which is exactly how Ctrl+B /// managed to mean two different things without anyone noticing. /// [Fact] public void NoKeyIsClaimedTwiceOnTheSameLayer() { var duplicates = ShortcutTable.AllCapClaims() .GroupBy(c => c) .Where(g => g.Count() > 1) .Select(g => g.Key) .ToList(); Assert.True(duplicates.Count == 0, "these keys are claimed by more than one binding: " + string.Join(", ", duplicates)); } /// Every cap the table lights has to exist on the drawn board, or the binding is /// invisible: the map only renders ids present in KbRows. Alt+M was missing the other way /// round, absent from the table while the list advertised it. [Fact] public void EveryCapExistsOnTheDrawnKeyboard() { // KbRows lives in the WPF half, so the ids are mirrored here deliberately: this test is // the thing that fails if the two ever disagree, which is the point. var board = new HashSet { "Esc","F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","F11","F12", "Grave","D1","D2","D3","D4","D5","D6","D7","D8","D9","D0","Minus","Equals","Back", "Ins","Home","PgUp", "Tab","Q","W","E","R","T","Y","U","I","O","P","LBr","RBr","Bslash","Del","End","PgDn", "Caps","A","S","D","F","G","H","J","K","L","Semi","Quote","Enter", "Shift","Z","X","C","V","B","N","M","Comma","Period","Slash","RShift","Up", "Ctrl","Win","Alt","Space","RAlt","Menu","RCtrl","Left","Down","Right", }; var missing = ShortcutTable.KsAll .SelectMany(b => b.Caps) .Select(c => c.Id) .Distinct() .Where(id => !board.Contains(id)) .ToList(); Assert.True(missing.Count == 0, "these cap ids are not on the drawn keyboard: " + string.Join(", ", missing)); } [Fact] public void EveryLabelKeyExistsInEnglish() { var english = EnglishKeys(); var missing = ShortcutTable.KsAll .SelectMany(b => new[] { b.LabelKey }.Concat(b.Caps.Select(c => c.LabelKey))) .Where(k => k.Length > 0) .Distinct() .Where(k => !english.Contains(k)) .ToList(); Assert.True(missing.Count == 0, "these resource keys are referenced but not defined in en-US.xaml: " + string.Join(", ", missing)); } [Fact] public void EveryBindingBelongsToADeclaredSection() { var groups = ShortcutTable.KsGroups.Select(g => g.Cat).ToHashSet(); var orphans = ShortcutTable.KsAll.Select(b => b.Cat).Distinct() .Where(c => !groups.Contains(c)).ToList(); Assert.True(orphans.Count == 0, "these categories have bindings but no section: " + string.Join(", ", orphans)); } /// A section with no bindings would render as a heading over nothing. [Fact] public void EverySectionHasAtLeastOneBinding() { var used = ShortcutTable.KsAll.Select(b => b.Cat).ToHashSet(); var empty = ShortcutTable.KsGroups.Select(g => g.Cat).Where(c => !used.Contains(c)).ToList(); Assert.True(empty.Count == 0, "these sections would render empty: " + string.Join(", ", empty)); } /// The layer prefix is the only thing separating Shift+F3 from F3. A typo in the /// prefix silently lands the cap on the base layer, so the parse gets its own check. // Layer passed by name: KbLayer is internal, and an InlineData parameter has to be at least // as accessible as the public test method. [Theory] [InlineData("F3", "Base", "F3")] [InlineData("Ctrl:O", "Ctrl", "O")] [InlineData("CtrlShift:D1", "CtrlShift", "D1")] [InlineData("Shift:F9", "Shift", "F9")] [InlineData("Alt:M", "Alt", "M")] public void CapParsesItsLayerPrefix(string id, string layerName, string bare) { var cap = ShortcutTable.Cap(id); Assert.Equal(layerName, cap.Layer.ToString()); Assert.Equal(bare, cap.Id); } /// A cap without its own label inherits the row's; one with a label keeps it. That /// is what lets "Home / End" stay one row while Home and End caption separately. [Fact] public void CapLabelOverridesTheRowAndOtherwiseInherits() { var map = ShortcutTable.BuildMap(); Assert.Equal("Str_Kb_FirstPage", map[KbLayer.Base]["Home"].Label); Assert.Equal("Str_Kb_LastPage", map[KbLayer.Base]["End"].Label); Assert.Equal("Str_KS_Open", map[KbLayer.Ctrl]["O"].Label); // inherited } /// The three bindings 1.7.5 changed, pinned so a later edit cannot quietly undo /// them: the sidebar is F9 and Shift+F9, and Ctrl+B is bold rather than the sidebar. [Fact] public void TheSidebarIsOnF9AndCtrlBIsBold() { var map = ShortcutTable.BuildMap(); Assert.Equal("Str_KS_ToggleSidebar", map[KbLayer.Base]["F9"].Label); Assert.Equal("Str_KS_SidebarSide", map[KbLayer.Shift]["F9"].Label); Assert.Equal("Str_Lbl_Bold", map[KbLayer.Ctrl]["B"].Label); Assert.False(map[KbLayer.Ctrl].ContainsKey("B") && map[KbLayer.Ctrl]["B"].Label == "Str_KS_ToggleSidebar", "Ctrl+B must not be the sidebar again"); } /// Alt+M was in the list and missing from the map for as long as it existed. [Fact] public void AltMIsOnTheMap() { Assert.Equal("Str_Toolbar_Hide", ShortcutTable.BuildMap()[KbLayer.Alt]["M"].Label); } /// Gesture-only rows (the wheel ones, Ctrl+Scroll, Shift+Click, Middle drag) have /// no keycap by design. Anything else with no caps is likely an oversight, so the count is /// pinned rather than left to drift. [Fact] public void OnlyMouseGesturesHaveNoCaps() { // Asserted by label key, not by the Keys text: the key column is tokenized, so pinning // its literal spelling here would just break every time a token is renamed. var capless = ShortcutTable.KsAll.Where(b => b.Caps.Length == 0) .Select(b => b.LabelKey).ToList(); Assert.Equal( new[] { "Str_KS_ZoomCursor", "Str_KS_PanView", "Str_KS_CycleView", "Str_KS_AppSize", "Str_KS_MultiSelect" } .OrderBy(s => s, StringComparer.Ordinal).ToArray(), capless.OrderBy(s => s, StringComparer.Ordinal).ToArray()); } /// /// Every token in the key column must be one the renderer knows, or it prints raw to the /// user as "%shft%". The zoom pair is substituted from the keyboard layout rather than the /// locale (Services/KeyLayout.cs), so it is known-good without a resource key. /// [Fact] public void EveryKeyTokenIsResolvable() { var known = ShortcutTable.KeyTokens.Select(t => t.Token) .Concat(new[] { "%zin%", "%zout%" }) .ToHashSet(); var unknown = ShortcutTable.KsAll .SelectMany(b => Regex.Matches(b.Keys, "%[a-z]+%").Cast().Select(m => m.Value)) .Distinct() .Where(t => !known.Contains(t)) .ToList(); Assert.True(unknown.Count == 0, "these tokens have no resource key and would render raw: " + string.Join(", ", unknown)); } [Fact] public void EveryKeyTokenResourceExistsInEnglish() { var english = EnglishKeys(); var missing = ShortcutTable.KeyTokens.Select(t => t.Key) .Where(k => !english.Contains(k)).ToList(); Assert.True(missing.Count == 0, "key-name resources missing from en-US.xaml: " + string.Join(", ", missing)); } /// The point of #230: no bare English key name is left sitting in the table. Only /// letters, digits, F-numbers, arrows and the chord punctuation may appear literally. [Fact] public void NoUntranslatedKeyNamesRemainInTheTable() { var offenders = ShortcutTable.KsAll .Where(b => Regex.IsMatch(Regex.Replace(b.Keys, "%[a-z]+%", ""), @"\b(Ctrl|Alt|Shift|Delete|Enter|Escape|Esc|Menu|Home|End|PgUp|PgDn|Tab|Scroll|Click|Wheel|Space|Middle|drag|or)\b")) .Select(b => b.Keys) .ToList(); Assert.True(offenders.Count == 0, "these rows still carry hardcoded English key names: " + string.Join(" | ", offenders)); } } }