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 <noreply@openai.com>
Signed-off-by: James Maes <jmaes@dmdbrands.com>
This commit is contained in:
James MaesandCodex committed 2026-10-11 18:26:16 -05:00
1 parent 5803fc1b5a
commit aeeb28dd5d
13 files changed
+288 -19

No files matched your search

+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 |
+22
View File
@@ -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
+3
View File
@@ -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.
@@ -114,6 +114,16 @@ public sealed class AppSettings : ShellSettings
/// <summary>Long Contents entries wrap in full; otherwise they use one line with an ellipsis and full-title tooltip.</summary>
public bool WrapOutlineEntries { get; set; }
/// <summary>Optional native depth markers distinguish heading levels without changing titles or identities.</summary>
public bool ShowOutlineDepthMarkers { get; set; }
/// <summary>Last native sidebar width in device-independent pixels.</summary>
public double OutlineWidth { get; set; } = 220;
+11
View File
@@ -9,6 +9,17 @@ namespace MarkdownViewr.Windows;
/// <summary>Startup preferences are shared by independent reader windows without changing an in-progress launch.</summary>
public partial class App
{
/// <summary>Updates native Contents and saved baselines only after the shared presentation preferences were persisted.</summary>
internal void ApplyContentsSettings(Core.AppSettings settings)
{
foreach(MainWindow window in _windows.ToArray())
{
window.ApplyContentsSettings(settings);
}
}
/// <summary>Updates each native reader and its saved baseline only after startup preferences were persisted successfully.</summary>
internal void ApplyStartupSettings(Core.AppSettings settings)
{
@@ -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)
@@ -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
/// <summary>Only Contents presentation fields change in the latest store, preserving sibling reader and CSS preferences.</summary>
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;
}
/// <summary>Committed Contents presentation refreshes live headings and the baseline without changing visibility or depth selection.</summary>
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);
}
/// <summary>Only fixed native Settings controls launch configuration; document content cannot request these operations.</summary>
private Task RunSettingsActionAsync(string action) => action switch
{
@@ -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;
/// <summary>General settings expose native Contents presentation and independent new-window defaults.</summary>
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<bool, bool, bool> _saveContents;
/// <summary>Related controls share one page; presentation updates live while panel defaults apply to new windows.</summary>
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 });
}
/// <summary>Saved sibling changes refresh the toggles without creating recursive saves.</summary>
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;
}
}
/// <summary>Persistence precedes visible changes; failure stays visible in the native status area.</summary>
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);
}
}
}
@@ -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;
}
+7 -3
View File
@@ -33,13 +33,14 @@ internal sealed partial class SettingsWindow : Window
/// <summary>Settings is a separate window; routine layout saves immediately and advanced CSS has an explicit Apply action.</summary>
public SettingsWindow(Core.AppSettings current, Func<ReadingAppearance, Task<bool>> saveAppearance, Func<bool, int, bool, bool, bool, bool> saveStartup, Func<string, Task> runAction)
public SettingsWindow(Core.AppSettings current, Func<ReadingAppearance, Task<bool>> saveAppearance, Func<bool, int, bool, bool, bool, bool> saveStartup, Func<bool, bool, bool> saveContents, Func<string, Task> 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);
}
/// <summary>Native layout controls save their validated changes; unsubmitted CSS stays a separate draft.</summary>
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");
@@ -49,6 +49,31 @@ public sealed class SettingsChangesTests
/// <summary>Legacy settings use truncated unmarked Contents; saved presentation choices survive an unrelated stale-reader edit.</summary>
[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);
}
/// <summary>A window's source change preserves newer history, theme, editor and print choices from its sibling.</summary>
[Fact]
public void Merge_StaleWindow_PreservesUnchangedSiblingPreferences()
@@ -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
/// <summary>Native long titles wrap without truncation, depth markers update sibling windows and unrelated saves retain both choices.</summary>
[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<string, string>(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");
}
/// <summary>The appearance draft survives a cancelled secondary theme editor without saving either draft.</summary>
[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
/// <summary>Reads the actual saved presentation choices and proves they did not opt new windows into either pane.</summary>
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());
}
/// <summary>All configuration tests enter through the actual File Settings command and native category items.</summary>
private static void OpenSettingsAction(AppSession session, string identity, string tab = "SettingsThemesTab")
{