feat(frontmatter): add persisted metadata display modes

Refs #8, #7. Also verify the native code-font size control.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <jmaes@dmdbrands.com>
This commit is contained in:
James MaesandCodex committed 2026-10-10 20:58:14 -05:00
1 parent d3530d106f
commit 3b6ebf83d0
14 files changed
+325 -10

No files matched your search

+1
View File
@@ -33,6 +33,7 @@ Prerelease tags (`v1.2.3-beta.1`) publish to the `dev` channel
- **File → Reload:** reread the current file manually. Local images stay within the document folder; remote images and authored scripts/styles are blocked.
- **View → Document theme:** choose one of Darin's 20 built-in palettes. The saved document palette is independent of the Windows shell theme; changing it retains the reading position.
- **View → Edit document theme:** edit colors, code font/size and custom CSS. Keep its name to override a built-in, or enter a new name to create a custom theme. Invalid input keeps the editor open. **Restore built-in themes** removes built-in edits after confirmation and keeps separately named custom themes.
- **View → Frontmatter:** hide leading YAML by default, show an escaped metadata table, or render the block as Markdown. The mode is saved and applied to reloads.
- **View:** choose the light, dark, or system shell theme; turn the startup splash and the
startup update check on or off. Choices are saved immediately.
- **Help → Activity log:** a live view of the app's log, with an errors-only filter and a
+2 -2
View File
@@ -108,9 +108,9 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## markdownViewr built-in themes
## markdownViewr resources and frontmatter behavior
The 20 original JSON themes are copied unchanged from Darin Kelkhoff's markdownViewr at commit 6d08bc9d27d7d7b2c2fa95bb282da330b8cf9021.
Frontmatter delimiter/metadata display behavior is adapted from MarkdownDocument.swift at the same pinned commit. The 20 original JSON themes are copied unchanged from Darin Kelkhoff's markdownViewr at commit 6d08bc9d27d7d7b2c2fa95bb282da330b8cf9021.
Source: <https://github.com/darinkelkhoff/markdownViewr/tree/6d08bc9d27d7d7b2c2fa95bb282da330b8cf9021/markdownViewr/Resources/themes>
+15 -1
View File
@@ -32,9 +32,23 @@ Built-in selection persists independently of the AppKit shell; actual dark/light
and screenshots passed. Current local change adds saved user overrides/custom names,
native color/code-font/size/CSS controls, invalid-input validation and built-in restoration
that retains custom names. AppKit saves preferences atomically; corrupt saved themes are
discarded before catalog loading. Core 119 tests/100% coverage and zero-warning build
discarded before catalog loading. Core 149 tests/100% coverage and zero-warning build
pass. New dialog/browser tests added but not run locally; next hosted acceptance pending.
Editable theme commit 30ca0943c09a8662794c2d400f1b5c49de426942 is now pushed.
Its CI 38102867556 fully passed, including new theme editor/color/CSS/invalid-input/
restore tests and both installed channels. CodeQL 38102867583 and scripts 38102867251
also passed. New follow-up tests the NumberBox range-value edit to font size 17 and
adds analyzer filename/full-exception reporting without changing rules or retry policy.
Frontmatter #8 now implements the reference's hidden default, escaped line-based
metadata and sanitized Markdown display. Complete leading delimiters required;
Windows CRLF and body preserved. Native mode changes persist and retain relative
viewport; menu disabled while snapshot loading prevents stale mode changes. 149 core
tests pass with 100% line coverage; native switching/table disappearance test added,
not run locally. Darin's MIT notice and source attribution retained. Next push waits
for these final local gates; observe its exact CI handles.
## Authority and continuity
Owner authorized signed/DCO commits, pushes, repository configuration and ticket updates.
+3 -1
View File
@@ -17,7 +17,9 @@
- [x] Implement persisted editable theme overrides/custom names, color/code-font/CSS controls and built-in restoration.
- [ ] Complete hosted theme editor/validation/restoration acceptance (issue #7).
- [ ] Add TOC, section collapse, source pane, synchronized navigation, find and vim keys.
- [ ] Add YAML frontmatter modes, safe configured editor launch and printing options.
- [x] Implement and core-test hidden/metadata/Markdown frontmatter modes, with persisted native controls and attribution.
- [ ] Verify actual hosted frontmatter mode switching and code-font size editing.
- [ ] Add safe configured editor launch and printing options.
- [ ] Add multiple document windows, recent files and Windows activation/file associations.
- [ ] Verify hosted checks on each actual implementation head; PSScriptAnalyzer engine crash remains tracked in #5.
- [ ] Finish standards inventory and reviewed/published AppKit/signing API adoption when available.
+1
View File
@@ -20,6 +20,7 @@ physical directory. It does not authorize native commands or remote resource acc
| Interface | Control |
| --- | --- |
| Picker / literal process argument | Read-only bounded snapshot; no shell interpolation |
| Frontmatter | Bounded by document limit; complete leading delimiters only; metadata values HTML-escaped; Markdown mode uses the same sanitizer; no YAML execution or native commands |
| Markdown / raw HTML | Explicit tag/attribute/scheme allowlist; disabled checkbox inputs; no authored scripts, CSS, IDs or classes |
| File change notifications | Directory subscription scoped to selected path; 350 ms quiet-period debounce; replacement/deletion recovery; bounded transient retries; subscriptions released on window close |
| Theme JSON / CSS | Native editor and saved user overrides use the same bounded validation; corrupt theme settings are discarded before startup; restoration keeps separately named custom themes; embedded original palettes; bounded schema-version-one parser; validated hex colors and finite sizes; code-point escaped font names; less-than signs escaped before style embedding; no network capability |
@@ -4,6 +4,7 @@
*/
using System.Text.Json;
using System.Text.Json.Serialization;
using KofTwentyTwo.AppKit.Settings;
@@ -21,10 +22,21 @@ public sealed class AppSettings : ShellSettings
/// <summary>The frontmatter display mode, saved independently of document contents.</summary>
public string Frontmatter { get; set; } = "Hide";
/// <summary>Repairs unknown saved mode values to the reference's default.</summary>
[JsonIgnore]
public Documents.FrontmatterMode FrontmatterKind => Enum.TryParse(Frontmatter, out Documents.FrontmatterMode mode) && Enum.IsDefined(mode) ? mode : Documents.FrontmatterMode.Hide;
/// <inheritdoc/>
public override void Sanitize()
{
base.Sanitize();
Frontmatter = FrontmatterKind.ToString();
if(string.IsNullOrWhiteSpace(DocumentTheme) || DocumentTheme.Length > 80 || DocumentTheme.Any(char.IsControl))
{
DocumentTheme = "Catppuccin Mocha";
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2026 Darin Kelkhoff
* Copyright (c) 2026 James Maes
* SPDX-License-Identifier: MIT
*/
using System.Net;
using System.Text;
namespace MarkdownViewr.Windows.Core.Documents;
/// <summary>Adapts Darin's delimiter and line-based metadata display without interpreting YAML as commands.</summary>
internal static class DocumentFrontmatter
{
/// <summary>An absent closing delimiter leaves the original document untouched.</summary>
public static (string? Metadata, string Body) Split(string markdown)
{
string[] lines = markdown.Split('\n');
if(lines.Length < 2 || !string.Equals(lines[0].Trim(), "---", StringComparison.Ordinal))
{
return (null, markdown);
}
int end = Array.FindIndex(lines, 1, line => string.Equals(line.Trim(), "---", StringComparison.Ordinal));
if(end < 0)
{
return (null, markdown);
}
return (string.Join('\n', lines[1..end]), string.Join('\n', lines[(end + 1)..]));
}
/// <summary>Metadata is display text: split only at the first colon and escape every value before creating table markup.</summary>
public static string RenderMetadata(string metadata)
{
var html = new StringBuilder("<table>");
foreach(string line in metadata.Split('\n'))
{
string text = line.Trim();
if(text.Length == 0)
{
continue;
}
int colon = text.IndexOf(':');
html.Append("<tr>");
if(colon >= 0)
{
html.Append("<td>").Append(WebUtility.HtmlEncode(PlainValue(text[..colon]))).Append("</td>");
html.Append("<td>").Append(WebUtility.HtmlEncode(PlainValue(text[(colon + 1)..]))).Append("</td>");
}
else
{
html.Append("<td colspan=\"2\">").Append(WebUtility.HtmlEncode(PlainValue(text))).Append("</td>");
}
html.Append("</tr>");
}
return html.Append("</table>").ToString();
}
/// <summary>Matches the reference's plain metadata labels while preserving mid-word punctuation.</summary>
private static string PlainValue(string text)
{
foreach(string marker in new[] { "***", "**", "__", "~~" })
{
text = text.Replace(marker, "", StringComparison.Ordinal);
}
text = text.Trim();
foreach(char marker in new[] { '*', '_', '`' })
{
if(text.StartsWith(marker) && text.EndsWith(marker))
{
text = text.Length > 1 ? text[1..^1] : "";
}
}
return text.Trim();
}
}
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2026 Darin Kelkhoff
* Copyright (c) 2026 James Maes
* SPDX-License-Identifier: MIT
*/
namespace MarkdownViewr.Windows.Core.Documents;
/// <summary>The reference app's three ways to display a leading frontmatter block.</summary>
public enum FrontmatterMode
{
/// <summary>Keep metadata out of the reading surface.</summary>
Hide,
/// <summary>Show escaped, line-based key/value rows.</summary>
Metadata,
/// <summary>Render the frontmatter block through the normal Markdown boundary.</summary>
Markdown,
}
@@ -47,14 +47,25 @@ public sealed class MarkdownRenderer
/// <summary>Preserves useful markup while sanitizing every parser result, including authored raw HTML.</summary>
public string Render(string markdown)
public string Render(string markdown, FrontmatterMode frontmatter = FrontmatterMode.Hide)
{
ArgumentNullException.ThrowIfNull(markdown);
if(markdown.Length > MaximumCharacters)
{
throw new ArgumentException("The document exceeds the 16 Mi-character rendering limit.", nameof(markdown));
}
return _sanitizer.Sanitize(Markdown.ToHtml(markdown, s_pipeline));
if(!Enum.IsDefined(frontmatter))
{
throw new ArgumentOutOfRangeException(nameof(frontmatter));
}
(string? metadata, string body) = DocumentFrontmatter.Split(markdown);
string prefix = !string.IsNullOrEmpty(metadata) ? frontmatter switch
{
FrontmatterMode.Metadata => DocumentFrontmatter.RenderMetadata(metadata),
FrontmatterMode.Markdown => Markdown.ToHtml("---\n" + metadata + "\n---", s_pipeline),
_ => "",
} : "";
return _sanitizer.Sanitize(prefix + Markdown.ToHtml(body, s_pipeline));
}
@@ -31,6 +31,12 @@
<MenuFlyoutItem x:Name="EditDocumentThemeMenu" Text="Edit document theme..." Click="EditDocumentTheme_Click" />
<MenuFlyoutItem x:Name="RestoreDocumentThemesMenu" Text="Restore built-in themes" Click="RestoreDocumentThemes_Click" />
<MenuFlyoutSeparator />
<MenuFlyoutSubItem x:Name="FrontmatterMenu" Text="Frontmatter">
<RadioMenuFlyoutItem x:Name="FrontmatterHide" Text="Hide" GroupName="Frontmatter" Tag="Hide" Click="Frontmatter_Click" />
<RadioMenuFlyoutItem x:Name="FrontmatterMetadata" Text="Show as metadata" GroupName="Frontmatter" Tag="Metadata" Click="Frontmatter_Click" />
<RadioMenuFlyoutItem x:Name="FrontmatterMarkdown" Text="Show as Markdown" GroupName="Frontmatter" Tag="Markdown" Click="Frontmatter_Click" />
</MenuFlyoutSubItem>
<MenuFlyoutSeparator />
<RadioMenuFlyoutItem x:Name="ThemeSystem" Text="System theme" GroupName="Theme" Tag="System" Click="Theme_Click" />
<RadioMenuFlyoutItem x:Name="ThemeLight" Text="Light theme" GroupName="Theme" Tag="Light" Click="Theme_Click" />
<RadioMenuFlyoutItem x:Name="ThemeDark" Text="Dark theme" GroupName="Theme" Tag="Dark" Click="Theme_Click" />
+19 -3
View File
@@ -39,6 +39,7 @@ public sealed partial class MainWindow : Window, IDisposable
_themes = Core.Themes.ThemeCatalog.Combine(_settings.UserThemes.Select(Core.Themes.ReaderTheme.Parse));
_browser = new DocumentBrowser(DocumentView, message => StatusText.Text = message, ActiveDocumentTheme());
CreateDocumentThemeMenu();
(_settings.FrontmatterKind switch { Core.Documents.FrontmatterMode.Metadata => FrontmatterMetadata, Core.Documents.FrontmatterMode.Markdown => FrontmatterMarkdown, _ => FrontmatterHide }).IsChecked = true;
Title = AppIdentity.Info.DisplayName;
FitInitialWindow();
this.SetAppIcon(AppIdentity.Info);
@@ -172,12 +173,13 @@ public sealed partial class MainWindow : Window, IDisposable
}
OpenMenuItem.IsEnabled = false;
ReloadMenuItem.IsEnabled = false;
FrontmatterMenu.IsEnabled = false;
try
{
_browserInitialization ??= _browser.InitializeAsync();
await _browserInitialization;
Core.Documents.DocumentViewport viewport = reload ? await _browser.CaptureViewportAsync() : default;
(DocumentFiles files, string html) = await ReadDocumentAsync(path);
(DocumentFiles files, string html) = await ReadDocumentAsync(path, _settings.FrontmatterKind);
if(_closed)
{
return;
@@ -207,6 +209,7 @@ public sealed partial class MainWindow : Window, IDisposable
else
{
OpenMenuItem.IsEnabled = true;
FrontmatterMenu.IsEnabled = true;
ReloadMenuItem.IsEnabled = _documentPath is not null;
}
}
@@ -251,7 +254,7 @@ public sealed partial class MainWindow : Window, IDisposable
/// <summary>Retries only transient missing/sharing violations from an editor's replace/save operation.</summary>
private static async Task<(DocumentFiles Files, string Html)> ReadDocumentAsync(string path)
private static async Task<(DocumentFiles Files, string Html)> ReadDocumentAsync(string path, Core.Documents.FrontmatterMode frontmatter)
{
for(int attempt = 0; ; attempt++)
{
@@ -260,7 +263,7 @@ public sealed partial class MainWindow : Window, IDisposable
return await Task.Run(() =>
{
var document = DocumentFiles.Open(path);
return (document, new Core.Documents.MarkdownRenderer().Render(document.Markdown));
return (document, new Core.Documents.MarkdownRenderer().Render(document.Markdown, frontmatter));
});
}
catch(IOException exception) when(attempt < 3 && (exception.HResult & 0xFFFF) is 2 or 3 or 32 or 33)
@@ -336,6 +339,19 @@ public sealed partial class MainWindow : Window, IDisposable
/// <summary>Persists frontmatter appearance and rereads the active snapshot at its reading position.</summary>
private async void Frontmatter_Click(object sender, RoutedEventArgs e)
{
_settings.Frontmatter = (string)((FrameworkElement)sender).Tag;
Save();
if(_documentPath is not null)
{
await OpenDocumentAsync(_documentPath, reload: true);
}
}
/// <summary>Resolves user overrides and safely falls back after a saved custom theme is removed.</summary>
private Core.Themes.ReaderTheme ActiveDocumentTheme()
=> _themes.FirstOrDefault(theme => string.Equals(theme.Name, _settings.DocumentTheme, StringComparison.Ordinal))
@@ -66,9 +66,10 @@ public class AppSettingsTests
{
using var temp = new TempDirectory();
var store = new KofTwentyTwo.AppKit.Settings.SettingsStore<AppSettings>(temp.File("settings.json"), AppSettingsContext.Default.AppSettings);
Assert.True(store.Save(new AppSettings { Theme = "Dark", DocumentTheme = "GitHub Light", UserThemes = [Themes.ThemeCatalog.Find("GitHub Light").Json] }));
Assert.True(store.Save(new AppSettings { Theme = "Dark", DocumentTheme = "GitHub Light", Frontmatter = "Metadata", UserThemes = [Themes.ThemeCatalog.Find("GitHub Light").Json] }));
Assert.Equal("Dark", store.Load().Theme);
Assert.Equal("GitHub Light", store.Load().DocumentTheme);
Assert.Equal(Documents.FrontmatterMode.Metadata, store.Load().FrontmatterKind);
Assert.Equal(Themes.ThemeCatalog.Find("GitHub Light").Json, Assert.Single(store.Load().UserThemes));
}
}
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2026 James Maes
* SPDX-License-Identifier: MIT
*/
using MarkdownViewr.Windows.Core.Documents;
namespace MarkdownViewr.Windows.Core.Tests;
/// <summary>Checks delimiter fidelity, escaped metadata and all three reference display modes.</summary>
public sealed class DocumentFrontmatterTests
{
/// <summary>Missing/unterminated delimiters preserve the complete original input.</summary>
[Theory]
[InlineData("")]
[InlineData("---")]
[InlineData("---\nunterminated")]
[InlineData("before\n---\nkey: value\n---")]
[InlineData("\n---\nkey: value\n---")]
[InlineData("----\nkey: value\n---")]
public void Split_NoCompleteLeadingBlock_PreservesDocument(string markdown)
{
(string? metadata, string body) = DocumentFrontmatter.Split(markdown);
Assert.Null(metadata);
Assert.Equal(markdown, body);
}
/// <summary>Windows newlines, whitespace delimiters, empty blocks and empty bodies are retained correctly.</summary>
[Theory]
[InlineData("---\nkey: value\n---\n# Body", "key: value", "# Body")]
[InlineData("---\r\nkey: value\r\n---\r\n# Body\r\n", "key: value\r", "# Body\r\n")]
[InlineData(" --- \nkey: value\n \t--- \nbody", "key: value", "body")]
[InlineData("---\n---", "", "")]
[InlineData("---\nkey: value\n---", "key: value", "")]
[InlineData("---\n\n---\nbody", "", "body")]
public void Split_CompleteBlock_PreservesMetadataAndBody(string markdown, string expectedMetadata, string expectedBody)
{
(string? metadata, string body) = DocumentFrontmatter.Split(markdown);
Assert.Equal(expectedMetadata, metadata);
Assert.Equal(expectedBody, body);
}
/// <summary>All modes retain the body; hide is the default and metadata never becomes active HTML.</summary>
[Fact]
public void Render_AllModes_PreservesBodyWithSafeMetadata()
{
const string Markdown = "---\ntitle: **Visible value**\nurl: https://example.com/a:b\n<script>bad()</script>\n\n---\n# Body heading";
var renderer = new MarkdownRenderer();
string hidden = renderer.Render(Markdown);
Assert.Contains("<h1>Body heading</h1>", hidden, StringComparison.Ordinal);
Assert.DoesNotContain("Visible value", hidden, StringComparison.Ordinal);
string metadata = renderer.Render(Markdown, FrontmatterMode.Metadata);
Assert.Contains("<td>title</td><td>Visible value</td>", metadata, StringComparison.Ordinal);
Assert.Contains("https://example.com/a:b", metadata, StringComparison.Ordinal);
Assert.Contains("&lt;script&gt;bad()&lt;/script&gt;", metadata, StringComparison.Ordinal);
Assert.DoesNotContain("<script", metadata, StringComparison.Ordinal);
Assert.Contains("<h1>Body heading</h1>", metadata, StringComparison.Ordinal);
string rendered = renderer.Render(Markdown, FrontmatterMode.Markdown);
Assert.Contains("<strong>Visible value</strong>", rendered, StringComparison.Ordinal);
Assert.DoesNotContain("<script", rendered, StringComparison.Ordinal);
Assert.Contains("<h1>Body heading</h1>", rendered, StringComparison.Ordinal);
}
/// <summary>Empty frontmatter emits no prefix; unknown enum values cannot change rendering policy.</summary>
[Fact]
public void Render_EmptyBlockAndInvalidMode_EnforcesContract()
{
var renderer = new MarkdownRenderer();
Assert.Equal("<p>Body</p>\n", renderer.Render("---\n---\nBody", FrontmatterMode.Metadata));
Assert.Throws<ArgumentOutOfRangeException>(() => renderer.Render("Body", (FrontmatterMode)99));
}
/// <summary>Metadata formatting follows the reference and never strips punctuation from inside words.</summary>
[Theory]
[InlineData("***Bold***", "Bold")]
[InlineData("__Bold__", "Bold")]
[InlineData("~~Removed~~", "Removed")]
[InlineData("*Italic*", "Italic")]
[InlineData("_Italic_", "Italic")]
[InlineData("`code`", "code")]
[InlineData("*", "")]
[InlineData("a_b", "a_b")]
[InlineData("O'Reilly", "O'Reilly")]
public void RenderMetadata_FormattingMarkers_ProducesPlainText(string value, string expected)
{
Assert.Equal("<table><tr><td>key</td><td>" + System.Net.WebUtility.HtmlEncode(expected) + "</td></tr></table>", DocumentFrontmatter.RenderMetadata("key: " + value));
}
/// <summary>Blank rows disappear and colon-free values span both columns.</summary>
[Fact]
public void RenderMetadata_UnstructuredRows_EscapesText()
{
Assert.Equal("<table><tr><td colspan=\"2\">A &amp; B</td></tr></table>", DocumentFrontmatter.RenderMetadata(" \nA & B\n\t"));
}
/// <summary>Saved mode values are normalized, while corrupt preferences return to hiding metadata.</summary>
[Theory]
[InlineData("Hide", FrontmatterMode.Hide)]
[InlineData("Metadata", FrontmatterMode.Metadata)]
[InlineData("Markdown", FrontmatterMode.Markdown)]
[InlineData("invalid", FrontmatterMode.Hide)]
[InlineData("99", FrontmatterMode.Hide)]
[InlineData(null, FrontmatterMode.Hide)]
public void Sanitize_SavedMode_UsesSupportedChoice(string? name, FrontmatterMode expected)
{
var settings = new AppSettings { Frontmatter = name! };
settings.Sanitize();
Assert.Equal(expected, settings.FrontmatterKind);
Assert.Equal(expected.ToString(), settings.Frontmatter);
}
}
@@ -118,6 +118,7 @@ public class AppTests
session.InvokeMenuItem("View", "EditDocumentThemeMenu");
session.EnterText("ThemeEditorName", "My reading theme");
session.EnterText("ThemeEditorCodeFont", "Consolas");
session.WaitForElement("ThemeEditorCodeSize").Patterns.RangeValue.Pattern.SetValue(17);
session.EnterText("ThemeEditorColor:background", "#112233");
session.EnterText("ThemeEditorCss", "html { background: #223344; }");
session.ClickButton("Save theme");
@@ -127,6 +128,7 @@ public class AppTests
Assert.Equal("My reading theme", settings.RootElement.GetProperty("DocumentTheme").GetString());
using var theme = JsonDocument.Parse(Assert.Single(settings.RootElement.GetProperty("UserThemes").EnumerateArray()).GetString()!);
Assert.Equal("Consolas", theme.RootElement.GetProperty("fonts").GetProperty("code").GetString());
Assert.Equal(17, theme.RootElement.GetProperty("sizes").GetProperty("codeFontSize").GetDouble());
Assert.Equal("#112233", theme.RootElement.GetProperty("colors").GetProperty("background").GetString());
Assert.Equal("html { background: #223344; }", theme.RootElement.GetProperty("customCSS").GetString());
Assert.NotNull(session.FindInApp(cf => cf.ByName("Custom theme acceptance")));
@@ -171,6 +173,32 @@ public class AppTests
}
/// <summary>The real reading surface switches between hidden, metadata-table and Markdown frontmatter modes.</summary>
[Fact]
public void Frontmatter_ChangeModes_UpdatesSurfaceAndSavesChoice()
{
using var session = new AppSession(document: "---\n**Visible metadata**\n---\n# Frontmatter body");
session.WaitFor(() => session.FindInApp(cf => cf.ByName("Frontmatter body")), "the document body", TimeSpan.FromSeconds(30));
Assert.Null(session.FindInApp(cf => cf.ByName("Visible metadata")));
session.InvokeMenuItem("View", "FrontmatterMenu");
session.ClickElement("FrontmatterMetadata");
session.WaitFor(() => session.FindInApp(cf => cf.ByName("Visible metadata")), "the metadata table value");
session.WaitFor(() => session.FindInApp(cf => cf.ByControlType(FlaUI.Core.Definitions.ControlType.Table)), "the frontmatter table");
session.InvokeMenuItem("View", "FrontmatterMenu");
session.ClickElement("FrontmatterMarkdown");
AppSession.WaitUntilGone(() => session.FindInApp(cf => cf.ByControlType(FlaUI.Core.Definitions.ControlType.Table)), "the metadata table after switching to Markdown");
session.WaitFor(() => session.FindInApp(cf => cf.ByName("Visible metadata")), "the rendered frontmatter heading");
session.WaitFor(
() => session.ReadSettings().Contains("\"Frontmatter\": \"Markdown\"", StringComparison.Ordinal) ? session.MainWindow : null,
"the Markdown frontmatter choice to be saved");
session.InvokeMenuItem("View", "FrontmatterMenu");
session.ClickElement("FrontmatterHide");
session.WaitFor(() => session.FindInApp(cf => cf.ByName("Reloaded document.md")), "the hidden-mode reload status");
AppSession.WaitUntilGone(() => session.FindInApp(cf => cf.ByName("Visible metadata")), "the hidden metadata");
Assert.NotNull(session.FindInApp(cf => cf.ByName("Frontmatter body")));
}
/// <summary>Updates: a build that was not installed by Velopack says updates need an install.</summary>
[Fact]
public void CheckForUpdates_InALooseBuild_SaysUpdatesNeedAnInstall()