From aeeb28dd5db3ab0e37ebcadd64fb2db7be8181f4 Mon Sep 17 00:00:00 2001 From: James Maes Date: Sun, 11 Oct 2026 18:26:16 -0500 Subject: [PATCH] feat(settings): add native Contents presentation preferences Move new-window panel defaults and file-type guidance into General. Add full heading wrapping, optional depth markers and sibling-safe saved presentation, retaining the native depth control and exact heading titles. Co-Authored-By: Codex Signed-off-by: James Maes --- .github/workflows/ci.yml | 2 +- docs/PRODUCT.md | 2 +- docs/SESSION-STATE.md | 22 +++++ docs/TODO.md | 3 + src/MarkdownViewr.Windows.Core/AppSettings.cs | 10 +++ src/MarkdownViewr.Windows/App.Startup.cs | 11 +++ .../MainWindow.Navigation.cs | 26 +++++- .../MainWindow.Settings.cs | 37 +++++++- .../SettingsWindow.Contents.cs | 85 +++++++++++++++++++ .../SettingsWindow.Startup.cs | 9 -- src/MarkdownViewr.Windows/SettingsWindow.cs | 10 ++- .../SettingsChangesTests.cs | 25 ++++++ .../AppTests.Settings.cs | 65 +++++++++++++- 13 files changed, 288 insertions(+), 19 deletions(-) create mode 100644 src/MarkdownViewr.Windows/SettingsWindow.Contents.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 319cadd..a41fcec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: shell: pwsh run: >- dotnet test tests/MarkdownViewr.Windows.UiTests --no-build - --filter 'FullyQualifiedName~SourceHighlighting|FullyQualifiedName~Settings_NewWindowPanelDefaults|FullyQualifiedName~ThemeTransfer_NativePickerCancels|FullyQualifiedName~Print_UserWidthAndBorders|FullyQualifiedName~ReadingKeys_ScrollCommands|FullyQualifiedName~Source_ShowReloadAndHide|FullyQualifiedName~ThemeEditor_AllTypography|FullyQualifiedName~ThemeEditor_NewCustomTheme|FullyQualifiedName~Appearance_WidthCssAndPrint|FullyQualifiedName~Toolbar_ReadingCommandsAndMenus|FullyQualifiedName~ThemeTransfer_BatchFutureInvalidAndDisabledExport|FullyQualifiedName~ReadingKeys_HeadingSequences|FullyQualifiedName~Source_BidirectionalScroll|FullyQualifiedName~Source_RawHtmlHeadingProvenance|FullyQualifiedName~ActivationInboxTests|FullyQualifiedName~Outline_NestedCollapse' + --filter 'FullyQualifiedName~SourceHighlighting|FullyQualifiedName~Settings_NewWindowPanelDefaults|FullyQualifiedName~Settings_ContentsPresentation|FullyQualifiedName~ThemeTransfer_NativePickerCancels|FullyQualifiedName~Print_UserWidthAndBorders|FullyQualifiedName~ReadingKeys_ScrollCommands|FullyQualifiedName~Source_ShowReloadAndHide|FullyQualifiedName~ThemeEditor_AllTypography|FullyQualifiedName~ThemeEditor_NewCustomTheme|FullyQualifiedName~Appearance_WidthCssAndPrint|FullyQualifiedName~Toolbar_ReadingCommandsAndMenus|FullyQualifiedName~ThemeTransfer_BatchFutureInvalidAndDisabledExport|FullyQualifiedName~ReadingKeys_HeadingSequences|FullyQualifiedName~Source_BidirectionalScroll|FullyQualifiedName~Source_RawHtmlHeadingProvenance|FullyQualifiedName~ActivationInboxTests|FullyQualifiedName~Outline_NestedCollapse' --logger 'trx;LogFileName=reader-regressions.trx' --results-directory artifacts/reader-regressions - name: Retain native regression evidence diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index d0b775f..1e808dc 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -26,7 +26,7 @@ Reference source inspected at commit `6d08bc9d27d7d7b2c2fa95bb282da330b8cf9021`. | Rendering | GitHub Flavored Markdown, tables, tasks, strikethrough, fenced code, local images/links; independently configurable highlight, superscript, subscript and underline extensions | | Reload | Watch saves, including atomic replacements; debounce changes, preserve scroll, report deletion/recovery | | Themes | 20 built-ins; editable body/heading/code fonts, sizes and palette; theme/global CSS; content width; theme order, enable/disable/delete/restore and cycling; JSON import/export with overwrite/keep-both/skip conflicts; persistent settings | -| Navigation | Resizable table of contents, depth control, heading jumps, collapsible sections and per-window font zoom (30–500%, Ctrl+plus/minus/0) | +| Navigation | Resizable table of contents, depth control, configurable wrapping or ellipsis/full-title tooltip, optional heading-depth markers, heading jumps, collapsible sections and per-window font zoom (30–500%, Ctrl+plus/minus/0) | | Source | Optional source pane, synchronized headings/scroll and collapse state; theme-aware syntax highlighting; draggable split opening at50/50 (#16/#17) | | Find | Match counts, next/previous matches, search in both panes and vim-style shortcuts | | Keys | j/k, Ctrl+d/u, gg/G, ]]/[[, /, n/N; Windows Ctrl-based equivalents for native commands | diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md index ebd80bb..d59f672 100644 --- a/docs/SESSION-STATE.md +++ b/docs/SESSION-STATE.md @@ -2,6 +2,28 @@ Updated: 2026-10-11 +## Native General and Contents options prepared - 2026-10-11 + +Move new-window Contents/Source defaults and Default Apps guidance into General; +splash/startup update controls remain Advanced. Add persisted Contents wrapping +and optional depth markers using native TextBlock/ListView/ToggleSwitch controls. +Unwrapped titles use one line/ellipsis and full-title tooltips; wrapped titles have +no two-line cap. Preserve heading identities, selected row, per-window depth and +visibility. Checked persistence precedes sibling broadcasts and baseline updates. +Category switches reset ordinary scroll position without discarding CSS drafts. + +All593 core tests pass with100% measured line coverage; zero-warning Release build +passes. A new actual native case checks long-title geometry, live sibling markers, +unrelated-save persistence, Settings reopening and restoration; focused/full/ +distributed suites retain every previous case and deadline. It has compiled but +not executed. Retain actual Settings-window screenshots in the hosted case. + +Signed local5803fc1 inline-heading fix and this preparation remain unpushed while +exact53755ab installed114596717826 is verified live; focused21/native99 on537 +pass. Do not cancel the current full job or claim new installed/chooser/signing +results. Issue21 remains open for full Settings redesign/default status/Printing/ +theme previews and owner acceptance. No local desktop input or production release. + ## Mac screenshot comparison and inline-heading correction - 2026-10-11 Reviewed all eight owner-provided Mac screenshots; issue #21 records the concrete diff --git a/docs/TODO.md b/docs/TODO.md index 123b513..a6726f8 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,5 +1,8 @@ # Development Tasks +- [x] Prepare native General Contents wrapping/depth-marker controls with checked sibling-safe persistence;593 core/100% measured lines and zero-warning Release pass. +- [ ] Execute the actual long-title/sibling/persistence/restoration test and inspect Settings screenshots on its exact pushed head. + - [x] Review all eight Mac screenshots and record verified Settings gaps in issue #21 without private content. - [ ] Implement/accept the native Windows Settings organization, General default-viewer status, Contents options, theme previews/defaults and Printing page. - [x] Reproduce/correct multiline inline raw-heading fold loss;592 core tests/100% measured lines and zero-warning Release pass. diff --git a/src/MarkdownViewr.Windows.Core/AppSettings.cs b/src/MarkdownViewr.Windows.Core/AppSettings.cs index e31def0..2e0b17b 100644 --- a/src/MarkdownViewr.Windows.Core/AppSettings.cs +++ b/src/MarkdownViewr.Windows.Core/AppSettings.cs @@ -114,6 +114,16 @@ public sealed class AppSettings : ShellSettings + /// Long Contents entries wrap in full; otherwise they use one line with an ellipsis and full-title tooltip. + public bool WrapOutlineEntries { get; set; } + + + + /// Optional native depth markers distinguish heading levels without changing titles or identities. + public bool ShowOutlineDepthMarkers { get; set; } + + + /// Last native sidebar width in device-independent pixels. public double OutlineWidth { get; set; } = 220; diff --git a/src/MarkdownViewr.Windows/App.Startup.cs b/src/MarkdownViewr.Windows/App.Startup.cs index 906be46..b1a1fc5 100644 --- a/src/MarkdownViewr.Windows/App.Startup.cs +++ b/src/MarkdownViewr.Windows/App.Startup.cs @@ -9,6 +9,17 @@ namespace MarkdownViewr.Windows; /// Startup preferences are shared by independent reader windows without changing an in-progress launch. public partial class App { + /// Updates native Contents and saved baselines only after the shared presentation preferences were persisted. + internal void ApplyContentsSettings(Core.AppSettings settings) + { + foreach(MainWindow window in _windows.ToArray()) + { + window.ApplyContentsSettings(settings); + } + } + + + /// Updates each native reader and its saved baseline only after startup preferences were persisted successfully. internal void ApplyStartupSettings(Core.AppSettings settings) { diff --git a/src/MarkdownViewr.Windows/MainWindow.Navigation.cs b/src/MarkdownViewr.Windows/MainWindow.Navigation.cs index 682de04..f3a132b 100644 --- a/src/MarkdownViewr.Windows/MainWindow.Navigation.cs +++ b/src/MarkdownViewr.Windows/MainWindow.Navigation.cs @@ -76,8 +76,30 @@ public sealed partial class MainWindow { foreach(Core.Documents.DocumentHeading heading in _outline.Headings.Where(heading => heading.Level <= _settings.OutlineDepth)) { - var label = new TextBlock { Tag = heading, Text = heading.Title, TextWrapping = TextWrapping.Wrap, MaxLines = 2, Margin = new Thickness((heading.Level - 1) * 12, 0, 0, 0) }; - var item = new ListViewItem { Content = label, Tag = heading }; + var label = new TextBlock + { + Tag = heading, + Text = heading.Title, + TextWrapping = _settings.WrapOutlineEntries ? TextWrapping.Wrap : TextWrapping.NoWrap, + TextTrimming = _settings.WrapOutlineEntries ? TextTrimming.None : TextTrimming.CharacterEllipsis, + MaxLines = _settings.WrapOutlineEntries ? 0 : 1, + }; + Microsoft.UI.Xaml.Automation.AutomationProperties.SetAutomationId(label, "OutlineTitle:" + heading.Index.ToString(System.Globalization.CultureInfo.InvariantCulture)); + var row = new Grid { Margin = new Thickness((heading.Level - 1) * 12, 0, 0, 0), ColumnSpacing = 6 }; + row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + if(_settings.ShowOutlineDepthMarkers) + { + var marker = new TextBlock { Text = "●○▪▫–·"[(heading.Level - 1)..heading.Level], VerticalAlignment = VerticalAlignment.Top, Width = 16 }; + Microsoft.UI.Xaml.Automation.AutomationProperties.SetAutomationId(marker, "OutlineDepthMarker:" + heading.Index.ToString(System.Globalization.CultureInfo.InvariantCulture)); + Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(marker, "Heading level " + heading.Level.ToString(System.Globalization.CultureInfo.InvariantCulture)); + row.Children.Add(marker); + } + Grid.SetColumn(label, 1); + row.Children.Add(label); + var item = new ListViewItem { Content = row, Tag = heading, HorizontalContentAlignment = HorizontalAlignment.Stretch }; + Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(item, heading.Title); + ToolTipService.SetToolTip(item, new TextBlock { Text = heading.Title, TextWrapping = TextWrapping.Wrap, MaxWidth = 320 }); Microsoft.UI.Xaml.Automation.AutomationProperties.SetAutomationId(item, "OutlineHeading:" + heading.Index.ToString(System.Globalization.CultureInfo.InvariantCulture)); OutlineList.Items.Add(item); if(heading == selected) diff --git a/src/MarkdownViewr.Windows/MainWindow.Settings.cs b/src/MarkdownViewr.Windows/MainWindow.Settings.cs index be8d2ae..ed7823d 100644 --- a/src/MarkdownViewr.Windows/MainWindow.Settings.cs +++ b/src/MarkdownViewr.Windows/MainWindow.Settings.cs @@ -40,7 +40,7 @@ public sealed partial class MainWindow } if(_settingsWindow is null) { - _settingsWindow = new SettingsWindow(_settings, SaveReadingAppearanceAsync, SaveStartupSettings, RunSettingsActionAsync); + _settingsWindow = new SettingsWindow(_settings, SaveReadingAppearanceAsync, SaveStartupSettings, SaveContentsSettings, RunSettingsActionAsync); WinUIShell.ApplyTheme((FrameworkElement)_settingsWindow.Content, _settings.ThemeKind); _settingsWindow.Closed += (_, _) => _settingsWindow = null; } @@ -113,6 +113,41 @@ public sealed partial class MainWindow + /// Only Contents presentation fields change in the latest store, preserving sibling reader and CSS preferences. + private bool SaveContentsSettings(bool wrap, bool depthMarkers) + { + if(_closed) + { + return false; + } + Core.AppSettings settings = AppIdentity.SettingsStore.Load(); + settings.WrapOutlineEntries = wrap; + settings.ShowOutlineDepthMarkers = depthMarkers; + if(!AppIdentity.SettingsStore.Save(settings)) + { + return false; + } + ((App)Application.Current).ApplyContentsSettings(settings); + return true; + } + + + + /// Committed Contents presentation refreshes live headings and the baseline without changing visibility or depth selection. + internal void ApplyContentsSettings(Core.AppSettings settings) + { + if(_closed) + { + return; + } + _settings.WrapOutlineEntries = _lastSavedSettings.WrapOutlineEntries = settings.WrapOutlineEntries; + _settings.ShowOutlineDepthMarkers = _lastSavedSettings.ShowOutlineDepthMarkers = settings.ShowOutlineDepthMarkers; + RefreshOutline(); + _settingsWindow?.UpdateContents(settings); + } + + + /// Only fixed native Settings controls launch configuration; document content cannot request these operations. private Task RunSettingsActionAsync(string action) => action switch { diff --git a/src/MarkdownViewr.Windows/SettingsWindow.Contents.cs b/src/MarkdownViewr.Windows/SettingsWindow.Contents.cs new file mode 100644 index 0000000..60517ff --- /dev/null +++ b/src/MarkdownViewr.Windows/SettingsWindow.Contents.cs @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026 James Maes + * SPDX-License-Identifier: MIT + */ + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Controls; + + +namespace MarkdownViewr.Windows; + +/// General settings expose native Contents presentation and independent new-window defaults. +internal sealed partial class SettingsWindow +{ + private readonly ToggleSwitch _wrapOutlineEntries = new() { Header = "Wrap long entries in Contents" }; + private readonly ToggleSwitch _showOutlineDepthMarkers = new() { Header = "Show heading-depth markers in Contents" }; + private readonly Func _saveContents; + + + + /// Related controls share one page; presentation updates live while panel defaults apply to new windows. + private void AddContentsPreferences(StackPanel page, Core.AppSettings current) + { + page.Children.Add(new TextBlock { Text = "New document windows", FontSize = 20, FontWeight = Microsoft.UI.Text.FontWeights.SemiBold }); + page.Children.Add(new TextBlock { Text = "Contents and Source start hidden by default. Toolbar changes affect only the current window.", TextWrapping = TextWrapping.Wrap }); + AutomationProperties.SetAutomationId(_showOutlineByDefault, "SettingsDefaultContentsToggle"); + AutomationProperties.SetAutomationId(_showSourceByDefault, "SettingsDefaultSourceToggle"); + _showOutlineByDefault.Toggled += (_, _) => SaveStartup(); + _showSourceByDefault.Toggled += (_, _) => SaveStartup(); + page.Children.Add(_showOutlineByDefault); + page.Children.Add(_showSourceByDefault); + page.Children.Add(new TextBlock { Text = "Contents", FontSize = 20, FontWeight = Microsoft.UI.Text.FontWeights.SemiBold }); + AutomationProperties.SetAutomationId(_wrapOutlineEntries, "SettingsContentsWrapToggle"); + AutomationProperties.SetAutomationId(_showOutlineDepthMarkers, "SettingsContentsDepthMarkersToggle"); + UpdateContents(current); + _wrapOutlineEntries.Toggled += (_, _) => SaveContents(); + _showOutlineDepthMarkers.Toggled += (_, _) => SaveContents(); + page.Children.Add(_wrapOutlineEntries); + page.Children.Add(new TextBlock { Text = "When off, long headings end with an ellipsis. Hover over an entry to see its full title.", TextWrapping = TextWrapping.Wrap, Opacity = 0.7 }); + page.Children.Add(_showOutlineDepthMarkers); + page.Children.Add(new TextBlock { Text = "A different shape marks each heading level. These changes apply to all open windows.", TextWrapping = TextWrapping.Wrap, Opacity = 0.7 }); + } + + + + /// Saved sibling changes refresh the toggles without creating recursive saves. + internal void UpdateContents(Core.AppSettings settings) + { + if(_closed) + { + return; + } + bool previous = _updating; + _updating = true; + try + { + _wrapOutlineEntries.IsOn = settings.WrapOutlineEntries; + _showOutlineDepthMarkers.IsOn = settings.ShowOutlineDepthMarkers; + } + finally + { + _updating = previous; + } + } + + + + /// Persistence precedes visible changes; failure stays visible in the native status area. + private void SaveContents() + { + if(_closed || _updating) + { + return; + } + try + { + _status.Text = _saveContents(_wrapOutlineEntries.IsOn, _showOutlineDepthMarkers.IsOn) ? "Saved. Applies to all open windows." : "Could not save the setting. Please try again."; + } + catch(Exception exception) + { + ReportError(exception); + } + } +} diff --git a/src/MarkdownViewr.Windows/SettingsWindow.Startup.cs b/src/MarkdownViewr.Windows/SettingsWindow.Startup.cs index a7cd224..501c20f 100644 --- a/src/MarkdownViewr.Windows/SettingsWindow.Startup.cs +++ b/src/MarkdownViewr.Windows/SettingsWindow.Startup.cs @@ -40,15 +40,6 @@ internal sealed partial class SettingsWindow page.Children.Add(_showSplash); page.Children.Add(_splashDuration); page.Children.Add(_checkStartupUpdates); - page.Children.Add(new TextBlock { Text = "New document windows", FontSize = 20, FontWeight = Microsoft.UI.Text.FontWeights.SemiBold }); - page.Children.Add(new TextBlock { Text = "Contents and Source start hidden by default. Toolbar changes affect only the current window.", TextWrapping = TextWrapping.Wrap }); - AutomationProperties.SetAutomationId(_showOutlineByDefault, "SettingsDefaultContentsToggle"); - AutomationProperties.SetAutomationId(_showSourceByDefault, "SettingsDefaultSourceToggle"); - _showOutlineByDefault.Toggled += (_, _) => SaveStartup(); - _showSourceByDefault.Toggled += (_, _) => SaveStartup(); - page.Children.Add(_showOutlineByDefault); - page.Children.Add(_showSourceByDefault); - AddDefaultFileTypes(page); return page; } diff --git a/src/MarkdownViewr.Windows/SettingsWindow.cs b/src/MarkdownViewr.Windows/SettingsWindow.cs index b3237a8..9e107ff 100644 --- a/src/MarkdownViewr.Windows/SettingsWindow.cs +++ b/src/MarkdownViewr.Windows/SettingsWindow.cs @@ -33,13 +33,14 @@ internal sealed partial class SettingsWindow : Window /// Settings is a separate window; routine layout saves immediately and advanced CSS has an explicit Apply action. - public SettingsWindow(Core.AppSettings current, Func> saveAppearance, Func saveStartup, Func runAction) + public SettingsWindow(Core.AppSettings current, Func> saveAppearance, Func saveStartup, Func saveContents, Func runAction) { Title = AppIdentity.Info.DisplayName + " — Settings"; SystemBackdrop = new MicaBackdrop(); _appearance = new ReadingAppearanceEditor(current.ReadingAppearance); _savedCss = current.GlobalCss; _saveStartup = saveStartup; + _saveContents = saveContents; _saveAppearance = saveAppearance; _runAction = runAction; _root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); @@ -55,7 +56,7 @@ internal sealed partial class SettingsWindow : Window AutomationProperties.SetLiveSetting(_status, AutomationLiveSetting.Polite); _root.Children.Add(_status); Content = _root; - AddPage("SettingsAppearanceTab", "Appearance", Symbol.Font, AppearancePage()); + AddPage("SettingsAppearanceTab", "General", Symbol.Home, AppearancePage(current)); AddPage("SettingsThemesTab", "Document themes", Symbol.Library, ThemesPage()); AddPage("SettingsEditorsTab", "Editors", Symbol.Edit, EditorsPage()); AddPage("SettingsAdvancedTab", "Advanced", Symbol.Setting, AdvancedPage(current)); @@ -139,14 +140,17 @@ internal sealed partial class SettingsWindow : Window _navigation.Header = title; _pageScroll.Content = null; _pageScroll.Content = page; + _pageScroll.ChangeView(null, 0, null, disableAnimation: true); } /// Native layout controls save their validated changes; unsubmitted CSS stays a separate draft. - private StackPanel AppearancePage() + private StackPanel AppearancePage(Core.AppSettings current) { var page = new StackPanel { Spacing = 16, MaxWidth = 720, HorizontalAlignment = HorizontalAlignment.Stretch }; + AddDefaultFileTypes(page); + AddContentsPreferences(page, current); page.Children.Add(_appearance.Fields); var apply = new Button { Content = "Apply custom CSS", HorizontalAlignment = HorizontalAlignment.Left }; AutomationProperties.SetAutomationId(apply, "SettingsApplyCss"); diff --git a/tests/MarkdownViewr.Windows.Core.Tests/SettingsChangesTests.cs b/tests/MarkdownViewr.Windows.Core.Tests/SettingsChangesTests.cs index d08dba1..bb11e74 100644 --- a/tests/MarkdownViewr.Windows.Core.Tests/SettingsChangesTests.cs +++ b/tests/MarkdownViewr.Windows.Core.Tests/SettingsChangesTests.cs @@ -49,6 +49,31 @@ public sealed class SettingsChangesTests + /// Legacy settings use truncated unmarked Contents; saved presentation choices survive an unrelated stale-reader edit. + [Fact] + public void Merge_ContentsPresentation_PersistsWithoutChangingPanelDefaults() + { + var before = new AppSettings(); + Assert.False(before.WrapOutlineEntries); + Assert.False(before.ShowOutlineDepthMarkers); + AppSettings edited = SettingsChanges.Copy(before); + edited.OutlineDepth = 5; + edited.ShowOutline = true; + var current = new AppSettings { WrapOutlineEntries = true, ShowOutlineDepthMarkers = true, DocumentTheme = "Nord" }; + AppSettings result = SettingsChanges.Copy(SettingsChanges.Merge(current, before, edited)); + Assert.True(result.WrapOutlineEntries); + Assert.True(result.ShowOutlineDepthMarkers); + Assert.True(result.ShowOutline); + Assert.Equal(5, result.OutlineDepth); + Assert.Equal("Nord", result.DocumentTheme); + Assert.False(result.ShowOutlineByDefault); + Assert.False(result.ShowSourceByDefault); + Assert.False(before.WrapOutlineEntries); + Assert.False(before.ShowOutlineDepthMarkers); + } + + + /// A window's source change preserves newer history, theme, editor and print choices from its sibling. [Fact] public void Merge_StaleWindow_PreservesUnchangedSiblingPreferences() diff --git a/tests/MarkdownViewr.Windows.UiTests/AppTests.Settings.cs b/tests/MarkdownViewr.Windows.UiTests/AppTests.Settings.cs index 8c948d7..8efedd8 100644 --- a/tests/MarkdownViewr.Windows.UiTests/AppTests.Settings.cs +++ b/tests/MarkdownViewr.Windows.UiTests/AppTests.Settings.cs @@ -25,7 +25,7 @@ public partial class AppTests Assert.Null(session.FindInApp(cf => cf.ByAutomationId("OutlineDepth"))); Assert.Null(session.FindInApp(cf => cf.ByAutomationId("SourceText"))); session.InvokeMenuItem("File", "SettingsMenuItem"); - session.WaitForElement("SettingsAdvancedTab").Patterns.SelectionItem.Pattern.Select(); + session.WaitForElement("SettingsAppearanceTab").Patterns.SelectionItem.Pattern.Select(); AutomationElement contents = session.WaitForElement("SettingsDefaultContentsToggle"); AutomationElement source = session.WaitForElement("SettingsDefaultSourceToggle"); Assert.Equal(ToggleState.Off, contents.Patterns.Toggle.Pattern.ToggleState.Value); @@ -56,6 +56,55 @@ public partial class AppTests + /// Native long titles wrap without truncation, depth markers update sibling windows and unrelated saves retain both choices. + [Fact] + public void Settings_ContentsPresentation_UpdatesLiveSiblingsAndPersists() + { + const string Title = "A deliberately long heading that must wrap across several lines in a narrow Contents panel without losing its full title"; + const string Document = "# Contents display fixture\n\n## " + Title + "\nBody"; + using var session = new AppSession(document: Document, documentFileName: "contents.md", + additionalDocuments: new Dictionary(StringComparer.Ordinal) { ["sibling.md"] = Document }); + Window primary = WaitForDocumentWindow(session, "contents.md"); + Window sibling = WaitForDocumentWindow(session, "sibling.md"); + WaitForWindowHeading(session, sibling, "Contents display fixture"); + session.WaitForElement("mv-heading-0"); + InvokeDocumentMenu(session, primary, "OutlineToggle", menuName: "View"); + InvokeDocumentMenu(session, sibling, "OutlineToggle", menuName: "View"); + AutomationElement title = WaitForWindowElement(session, primary, "OutlineTitle:1"); + double oneLineHeight = title.BoundingRectangle.Height; + Assert.True(oneLineHeight > 0); + Assert.Equal(Title, WaitForWindowElement(session, primary, "OutlineHeading:1").Name); + Assert.Null(session.FindInApp(cf => cf.ByAutomationId("OutlineDepthMarker:1"))); + InvokeDocumentMenu(session, primary, "SettingsMenuItem"); + AutomationElement wrap = session.WaitForElement("SettingsContentsWrapToggle"); + AutomationElement markers = session.WaitForElement("SettingsContentsDepthMarkersToggle"); + Assert.Equal(ToggleState.Off, wrap.Patterns.Toggle.Pattern.ToggleState.Value); + Assert.Equal(ToggleState.Off, markers.Patterns.Toggle.Pattern.ToggleState.Value); + wrap.Patterns.Toggle.Pattern.Toggle(); + markers.Patterns.Toggle.Pattern.Toggle(); + RetainDocumentWindow(session, session.TopLevelWindows().Single(element => string.Equals(element.Name, session.DisplayName + " — Settings", StringComparison.Ordinal)).AsWindow(), "general-contents-settings"); + CloseSettings(session); + session.WaitFor(() => primary.FindFirstDescendant(cf => cf.ByAutomationId("OutlineTitle:1")) is { } label && label.BoundingRectangle.Height > oneLineHeight * 1.8 ? label : null, "the fully wrapped Contents title"); + Assert.Equal("Heading level 2", WaitForWindowElement(session, primary, "OutlineDepthMarker:1").Name); + Assert.Equal("Heading level 2", WaitForWindowElement(session, sibling, "OutlineDepthMarker:1").Name); + Assert.True(WaitForWindowElement(session, sibling, "OutlineTitle:1").BoundingRectangle.Height > oneLineHeight * 1.8); + InvokeDocumentMenu(session, sibling, "SourceToggle", menuName: "View"); + AssertSavedContentsPreferences(session, enabled: true); + InvokeDocumentMenu(session, primary, "SettingsMenuItem"); + Assert.Equal(ToggleState.On, session.WaitForElement("SettingsContentsWrapToggle").Patterns.Toggle.Pattern.ToggleState.Value); + Assert.Equal(ToggleState.On, session.WaitForElement("SettingsContentsDepthMarkersToggle").Patterns.Toggle.Pattern.ToggleState.Value); + session.WaitForElement("SettingsContentsWrapToggle").Patterns.Toggle.Pattern.Toggle(); + session.WaitForElement("SettingsContentsDepthMarkersToggle").Patterns.Toggle.Pattern.Toggle(); + CloseSettings(session); + Assert.Null(session.FindInApp(cf => cf.ByAutomationId("OutlineDepthMarker:1"))); + session.WaitFor(() => primary.FindFirstDescendant(cf => cf.ByAutomationId("OutlineTitle:1")) is { } label && label.BoundingRectangle.Height <= oneLineHeight * 1.2 ? label : null, "the restored single-line Contents title"); + Assert.Equal(Title, WaitForWindowElement(session, primary, "OutlineHeading:1").Name); + AssertSavedContentsPreferences(session, enabled: false); + RetainDocumentWindow(session, primary, "contents-truncation-and-markers"); + } + + + /// The appearance draft survives a cancelled secondary theme editor without saving either draft. [Fact] public void Settings_ThemeEditorCancel_RetainsCssDraftUntilWindowClose() @@ -170,7 +219,7 @@ public partial class AppTests using var session = new AppSession(document: "# Default file settings fixture"); session.WaitFor(() => session.FindInApp(cf => cf.ByName("Default file settings fixture")), "the default file settings fixture", TimeSpan.FromSeconds(30)); session.InvokeMenuItem("File", "SettingsMenuItem"); - session.WaitForElement("SettingsAdvancedTab").Patterns.SelectionItem.Pattern.Select(); + session.WaitForElement("SettingsAppearanceTab").Patterns.SelectionItem.Pattern.Select(); AutomationElement guidance = session.WaitForElement("SettingsDefaultFilesGuidance"); Assert.Contains(".md", guidance.Name, StringComparison.Ordinal); Assert.Contains(".txt) is optional", guidance.Name, StringComparison.Ordinal); @@ -201,6 +250,18 @@ public partial class AppTests + /// Reads the actual saved presentation choices and proves they did not opt new windows into either pane. + private static void AssertSavedContentsPreferences(AppSession session, bool enabled) + { + using var saved = JsonDocument.Parse(session.ReadSettings()); + Assert.Equal(enabled, saved.RootElement.GetProperty("WrapOutlineEntries").GetBoolean()); + Assert.Equal(enabled, saved.RootElement.GetProperty("ShowOutlineDepthMarkers").GetBoolean()); + Assert.False(saved.RootElement.GetProperty("ShowOutlineByDefault").GetBoolean()); + Assert.False(saved.RootElement.GetProperty("ShowSourceByDefault").GetBoolean()); + } + + + /// All configuration tests enter through the actual File Settings command and native category items. private static void OpenSettingsAction(AppSession session, string identity, string tab = "SettingsThemesTab") {