feat(editor): configure literal external document launch
Persist a validated editor executable, expose native configuration and launch commands, and preserve the viewer on missing files or launch errors. Add bounded argument tests and hosted actual-process fixtures; editor-list and folder parity remain tracked in issue #11. Co-Authored-By: Codex (GPT-6) <noreply@openai.com> Signed-off-by: James Maes <jmaes@dmdbrands.com>
This commit is contained in:
13 files changed
+360
-3
No files matched your search
@@ -34,6 +34,7 @@ Prerelease tags (`v1.2.3-beta.1`) publish to the `dev` channel
|
||||
- **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 → Table of contents:** show the heading sidebar, choose levels 1–6, and drag its divider to resize. Select a heading to jump there; collapse a selected section or all sections, then expand when ready. Matching section state survives reloads and document theme changes.
|
||||
- **File → Configure editor / Open in editor:** save the full path of an installed `.exe`, then open the current document with one literal file argument. A missing editor or document reports a status message; the viewer remains open. Reference editor-list/folder-opening parity and hosted acceptance remain pending.
|
||||
- **Edit → Find / Ctrl+F:** search the rendered document and literal source, with one occurrence count and wrapping **Next/Previous** controls. Enter moves forward, Shift+Enter moves back, and Escape closes find. Search temporarily reveals collapsed sections; closing it restores prior choices and retains explicit section commands made during search.
|
||||
- **View → Markdown source:** show the literal, read-only snapshot beside the reader, including frontmatter and fenced code. Visibility and preferred divider width persist; saves refresh both surfaces. Heading/scroll/collapse synchronization and vim keys remain in development; paired-find desktop acceptance is pending.
|
||||
- **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.
|
||||
|
||||
@@ -205,3 +205,21 @@ and artifacts/window-find-installed-38109196556.log.
|
||||
Separate local editor work is tracked by issue #11: literal ProcessStartInfo
|
||||
ArgumentList, persisted executable selection and native controls. Its 195 core
|
||||
tests/100% lines pass; that work is not included in this keyboard correction.
|
||||
|
||||
## Configured editor increment prepared locally
|
||||
|
||||
Issue #11 tracks editor launch and the broader reference list/folder parity audit.
|
||||
The local increment adds a sanitized persisted executable path, native configure
|
||||
Save/Cancel and open commands, missing-editor/document status, and one literal
|
||||
ProcessStartInfo.ArgumentList value with UseShellExecute=false. No arguments or
|
||||
source content determine executable selection; operational logs omit paths/content.
|
||||
195 core tests pass with 100% measured lines; final native build has zero warnings
|
||||
and formatting passes. Three new hosted tests use the existing test-only UpdateProbe
|
||||
in an environment-selected capture mode; it never reads document contents. They
|
||||
verify actual process arguments/working directory, saved configuration, invalid
|
||||
selection/cancel and an editor removed after selection. No local GUI input occurred.
|
||||
|
||||
This editor work remains separate from pushed keyboard head 2292603 until its
|
||||
live CI 38109719180 ends. CodeQL 38109719207 remains under observation;
|
||||
scripts 38109718804, security 38109719182 and PR gates 38109719194 passed.
|
||||
Keyboard/fragment and editor hosted acceptance remain unverified; full scope active.
|
||||
+3
-1
@@ -27,7 +27,9 @@
|
||||
- [x] Implement and core-test hidden/metadata/Markdown frontmatter modes, with persisted native controls and attribution.
|
||||
- [x] Verify actual code-font size 17 editing in hosted UI runs.
|
||||
- [x] Verify actual hosted frontmatter mode switching on 4bf0b81 (including both installed channels).
|
||||
- [ ] Add safe configured editor launch and printing options.
|
||||
- [x] Implement bounded persisted editor selection and deliberate native launch using one literal process argument (issue #11).
|
||||
- [ ] Verify real hosted editor fixture launch, validation/cancel and missing-executable acceptance; audit editor list/folder parity.
|
||||
- [ ] Add native printing/theme/layout options and screen-state restoration.
|
||||
- [ ] Add multiple document windows, recent files and Windows activation/file associations.
|
||||
- [x] Verify the all-rule two-pass PSScriptAnalyzer mitigation on scripts 38104988748 and 38105483489; closes #5 only on reviewed merge.
|
||||
- [ ] Verify hosted checks on each actual implementation head.
|
||||
|
||||
@@ -79,3 +79,15 @@ through; closing the window removes the callback. No global keyboard registratio
|
||||
other-process input capture, arbitrary command dispatch, browser bridge or text
|
||||
logging is introduced. Actual hosted browser-focus acceptance is still required.
|
||||
See Microsoft's [thread-hook contract](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexw).
|
||||
|
||||
## Configured editor boundary
|
||||
|
||||
Only deliberate native configuration selects an existing, bounded, fully qualified
|
||||
`.exe`. AppKit sanitizes the saved choice. Opening the editor requires a current
|
||||
existing document and passes its full path as exactly one `ArgumentList` value with
|
||||
`UseShellExecute=false`; Markdown cannot choose an executable or supply switches.
|
||||
The chosen application runs with the user's permissions and receives the document.
|
||||
Missing files and native launch errors leave the viewer open; logs omit paths and
|
||||
contents. Core tests cover metacharacters/Unicode, relative/script paths, corruption
|
||||
and bounds. Actual fixture-process/UI acceptance is pending; reference editor-list
|
||||
and folder-opening parity still require review.
|
||||
@@ -56,10 +56,16 @@ public sealed class AppSettings : ShellSettings
|
||||
|
||||
|
||||
|
||||
/// <summary>The user-selected external editor; an empty value requires deliberate configuration.</summary>
|
||||
public string ExternalEditor { get; set; } = "";
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Sanitize()
|
||||
{
|
||||
base.Sanitize();
|
||||
ExternalEditor = Editors.EditorLaunch.IsExecutablePath(ExternalEditor) ? ExternalEditor : "";
|
||||
SourceWidth = double.IsFinite(SourceWidth) ? Math.Clamp(SourceWidth, 160, 600) : 320;
|
||||
OutlineDepth = Math.Clamp(OutlineDepth, 1, 6);
|
||||
OutlineWidth = double.IsFinite(OutlineWidth) ? Math.Clamp(OutlineWidth, 140, 500) : 220;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2026 James Maes
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
|
||||
namespace MarkdownViewr.Windows.Core.Editors;
|
||||
|
||||
/// <summary>Builds an explicit editor process without interpreting a document path as a shell command.</summary>
|
||||
public static class EditorLaunch
|
||||
{
|
||||
/// <summary>Only a bounded, fully qualified Windows executable can be persisted as the chosen editor.</summary>
|
||||
public static bool IsExecutablePath(string? path)
|
||||
=> path is { Length: > 0 and <= 32767 } && !path.Any(char.IsControl) && Path.IsPathFullyQualified(path) && string.Equals(Path.GetExtension(path), ".exe", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
|
||||
|
||||
/// <summary>Supplies exactly one literal file argument; filesystem availability is checked by the Windows adapter.</summary>
|
||||
public static ProcessStartInfo Create(string executable, string document)
|
||||
{
|
||||
if(!IsExecutablePath(executable))
|
||||
{
|
||||
throw new ArgumentException("Choose an editor executable using its full path.", nameof(executable));
|
||||
}
|
||||
if(string.IsNullOrEmpty(document) || document.Length > 32767 || document.Any(char.IsControl) || !Path.IsPathFullyQualified(document))
|
||||
{
|
||||
throw new ArgumentException("The document must have a full file path.", nameof(document));
|
||||
}
|
||||
var start = new ProcessStartInfo
|
||||
{
|
||||
FileName = executable,
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = Path.GetDirectoryName(document),
|
||||
};
|
||||
start.ArgumentList.Add(document);
|
||||
return start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2026 James Maes
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
|
||||
namespace MarkdownViewr.Windows;
|
||||
|
||||
/// <summary>Routes deliberate native editor commands through a literal process argument boundary.</summary>
|
||||
public sealed partial class MainWindow
|
||||
{
|
||||
/// <summary>A configured editor becomes available only after a document has been committed.</summary>
|
||||
private void RefreshEditorCommands()
|
||||
=> OpenEditorMenuItem.IsEnabled = _documentPath is not null && Core.Editors.EditorLaunch.IsExecutablePath(_settings.ExternalEditor);
|
||||
|
||||
|
||||
|
||||
/// <summary>Validates an installed executable before persisting the user's deliberate choice.</summary>
|
||||
private async void ConfigureEditor_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var path = new TextBox { Name = "EditorPath", Header = "Editor executable path", Text = _settings.ExternalEditor, MaxLength = 32767, MinWidth = 320 };
|
||||
var error = new TextBlock { Name = "EditorError", TextWrapping = TextWrapping.Wrap };
|
||||
var panel = new StackPanel { Spacing = 12 };
|
||||
panel.Children.Add(new TextBlock { Text = "Enter the full path of an installed Windows editor. It receives the current document as one file argument.", TextWrapping = TextWrapping.Wrap });
|
||||
panel.Children.Add(path);
|
||||
panel.Children.Add(error);
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = "Configure editor",
|
||||
Content = panel,
|
||||
PrimaryButtonText = "Save",
|
||||
CloseButtonText = "Cancel",
|
||||
DefaultButton = ContentDialogButton.Primary,
|
||||
XamlRoot = Content.XamlRoot,
|
||||
};
|
||||
dialog.PrimaryButtonClick += (_, args) =>
|
||||
{
|
||||
if(!Core.Editors.EditorLaunch.IsExecutablePath(path.Text) || !File.Exists(path.Text))
|
||||
{
|
||||
args.Cancel = true;
|
||||
error.Text = "Enter the full path of an existing .exe file.";
|
||||
}
|
||||
};
|
||||
if(await dialog.ShowAsync() != ContentDialogResult.Primary || _closed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_settings.ExternalEditor = path.Text;
|
||||
Save();
|
||||
RefreshEditorCommands();
|
||||
StatusText.Text = "Editor configured.";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>Launches only the selected executable and never interpolates Markdown or a shell command.</summary>
|
||||
private void OpenEditor_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if(_closed || _documentPath is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(!File.Exists(_settings.ExternalEditor))
|
||||
{
|
||||
StatusText.Text = "The configured editor is missing. Use File → Configure editor to choose an installed executable.";
|
||||
return;
|
||||
}
|
||||
if(!File.Exists(_documentPath))
|
||||
{
|
||||
StatusText.Text = "The document is missing. Restore it before opening the editor.";
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
using var process = Process.Start(Core.Editors.EditorLaunch.Create(_settings.ExternalEditor, _documentPath));
|
||||
StatusText.Text = process is not null ? "Opened document in the configured editor." : "The configured editor did not start.";
|
||||
App.Log.Info("Requested a document launch in the configured editor.");
|
||||
}
|
||||
catch(Exception exception) when(exception is System.ComponentModel.Win32Exception or IOException or UnauthorizedAccessException or ArgumentException or InvalidOperationException)
|
||||
{
|
||||
StatusText.Text = "Could not start the configured editor. Use File → Configure editor to check its executable.";
|
||||
App.Log.Warning("The configured editor could not be started.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,9 @@
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem x:Name="ReloadMenuItem" Text="Reload" Click="Reload_Click" IsEnabled="False" />
|
||||
<MenuFlyoutSeparator />
|
||||
<MenuFlyoutItem x:Name="OpenEditorMenuItem" Text="Open in editor" Click="OpenEditor_Click" IsEnabled="False" />
|
||||
<MenuFlyoutItem x:Name="ConfigureEditorMenuItem" Text="Configure editor..." Click="ConfigureEditor_Click" />
|
||||
<MenuFlyoutSeparator />
|
||||
<MenuFlyoutItem x:Name="ExitMenuItem" Text="Exit" Click="Exit_Click" />
|
||||
</MenuBarItem>
|
||||
<MenuBarItem Title="Edit">
|
||||
|
||||
@@ -42,6 +42,7 @@ public sealed partial class MainWindow : Window, IDisposable
|
||||
InitializeOutline();
|
||||
InitializeSource();
|
||||
InitializeFind();
|
||||
RefreshEditorCommands();
|
||||
InitializeInputDiagnostics();
|
||||
(_settings.FrontmatterKind switch { Core.Documents.FrontmatterMode.Metadata => FrontmatterMetadata, Core.Documents.FrontmatterMode.Markdown => FrontmatterMarkdown, _ => FrontmatterHide }).IsChecked = true;
|
||||
Title = AppIdentity.Info.DisplayName;
|
||||
@@ -290,6 +291,7 @@ public sealed partial class MainWindow : Window, IDisposable
|
||||
_outline = outline;
|
||||
RefreshOutline();
|
||||
_documentPath = Path.GetFullPath(path);
|
||||
RefreshEditorCommands();
|
||||
Title = Path.GetFileName(path) + " — " + AppIdentity.Info.DisplayName;
|
||||
StatusText.Text = (reload ? "Reloaded " : "Opened ") + Path.GetFileName(path);
|
||||
App.Log.Info(reload ? "Reloaded a Markdown document." : "Opened a Markdown document.");
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (c) 2026 James Maes
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
using MarkdownViewr.Windows.Core.Editors;
|
||||
|
||||
|
||||
namespace MarkdownViewr.Windows.Core.Tests;
|
||||
|
||||
/// <summary>Proves chosen executable boundaries and literal document arguments without launching an external application.</summary>
|
||||
public sealed class EditorLaunchTests
|
||||
{
|
||||
/// <summary>Whitespace, command punctuation and Unicode remain one process argument rather than shell syntax.</summary>
|
||||
[Theory]
|
||||
[InlineData(@"C:\Documents\my notes.md")]
|
||||
[InlineData(@"C:\Documents\日本語 café.md")]
|
||||
[InlineData(@"C:\Documents\x & calc; $HOME `whoami`.md")]
|
||||
[InlineData(@"C:\Documents\--reuse-window.md")]
|
||||
public void Create_LiteralDocument_PreservesSingleArgument(string document)
|
||||
{
|
||||
System.Diagnostics.ProcessStartInfo start = EditorLaunch.Create(@"C:\Program Files\Editor\Editor.EXE", document);
|
||||
Assert.Equal(@"C:\Program Files\Editor\Editor.EXE", start.FileName);
|
||||
Assert.False(start.UseShellExecute);
|
||||
Assert.Equal(@"C:\Documents", start.WorkingDirectory);
|
||||
Assert.Equal(document, Assert.Single(start.ArgumentList));
|
||||
Assert.Empty(start.Arguments);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>Relative commands, scripts, malformed saved settings and oversized paths cannot select the editor.</summary>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData("editor.exe")]
|
||||
[InlineData(@"C:editor.exe")]
|
||||
[InlineData(@"C:\editor.cmd")]
|
||||
[InlineData("C:\\editor\n.exe")]
|
||||
public void Create_InvalidExecutable_RejectsChoice(string? executable)
|
||||
{
|
||||
Assert.False(EditorLaunch.IsExecutablePath(executable));
|
||||
Assert.Throws<ArgumentException>(() => EditorLaunch.Create(executable!, @"C:\Documents\test.md"));
|
||||
var settings = new AppSettings { ExternalEditor = executable! };
|
||||
settings.Sanitize();
|
||||
Assert.Empty(settings.ExternalEditor);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>Saved paths are bounded independently of filesystem existence.</summary>
|
||||
[Fact]
|
||||
public void Sanitize_ExecutableLength_RepairsOversizedChoiceAndRetainsValidChoice()
|
||||
{
|
||||
string oversized = "C:\\" + new string('a', 32767) + ".exe";
|
||||
Assert.False(EditorLaunch.IsExecutablePath(oversized));
|
||||
Assert.Throws<ArgumentException>(() => EditorLaunch.Create(oversized, @"C:\Documents\test.md"));
|
||||
var settings = new AppSettings { ExternalEditor = oversized };
|
||||
settings.Sanitize();
|
||||
Assert.Empty(settings.ExternalEditor);
|
||||
settings.ExternalEditor = @"C:\Editor\editor.exe";
|
||||
settings.Sanitize();
|
||||
Assert.Equal(@"C:\Editor\editor.exe", settings.ExternalEditor);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>An incomplete or corrupt document path cannot be passed to an editor.</summary>
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData("relative.md")]
|
||||
[InlineData("C:\\Documents\\bad\nname.md")]
|
||||
public void Create_InvalidDocument_RejectsArgument(string? document)
|
||||
=> Assert.Throws<ArgumentException>(() => EditorLaunch.Create(@"C:\Editor\editor.exe", document!));
|
||||
|
||||
|
||||
|
||||
/// <summary>Oversized document paths are rejected before process construction.</summary>
|
||||
[Fact]
|
||||
public void Create_OversizedDocument_RejectsArgument()
|
||||
=> Assert.Throws<ArgumentException>(() => EditorLaunch.Create(@"C:\Editor\editor.exe", "C:\\" + new string('x', 32767)));
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public sealed class AppSession : IDisposable
|
||||
|
||||
|
||||
/// <summary>Launches the app against a fresh temporary data folder and waits for its main window.</summary>
|
||||
public AppSession(bool showSplash = false, string? document = null)
|
||||
public AppSession(bool showSplash = false, string? document = null, bool editorProbe = false, string documentFileName = "document.md")
|
||||
{
|
||||
ExePath = ResolveExePath();
|
||||
DataDirectory = Path.Combine(Path.GetTempPath(), AppIdentity.Info.Id + "-uitests", Guid.NewGuid().ToString("N"));
|
||||
@@ -44,9 +44,13 @@ public sealed class AppSession : IDisposable
|
||||
var startInfo = new ProcessStartInfo(ExePath) { WorkingDirectory = Path.GetDirectoryName(ExePath) ?? "" };
|
||||
startInfo.Environment[AppIdentity.Info.DataDirectoryVariable] = DataDirectory;
|
||||
startInfo.Environment["MARKDOWNVIEWR_INPUT_TRACE"] = "1";
|
||||
if(editorProbe)
|
||||
{
|
||||
startInfo.Environment["MARKDOWNVIEWR_EDITOR_PROBE_RESULT"] = Path.Combine(DataDirectory, "editor-result.json");
|
||||
}
|
||||
if(document is not null)
|
||||
{
|
||||
string path = Path.Combine(DataDirectory, "document.md");
|
||||
string path = Path.Combine(DataDirectory, documentFileName);
|
||||
File.WriteAllText(path, document);
|
||||
startInfo.ArgumentList.Add(path);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,85 @@ public class AppTests
|
||||
|
||||
|
||||
|
||||
/// <summary>The native command launches the chosen real fixture process with one literal document path.</summary>
|
||||
[Fact]
|
||||
public void Editor_ConfiguredExecutable_LaunchesLiteralDocument()
|
||||
{
|
||||
using var session = new AppSession(document: "# Editor fixture", editorProbe: true, documentFileName: "notes & café $HOME.md");
|
||||
session.WaitFor(() => session.FindInApp(cf => cf.ByName("Editor fixture")), "the editor fixture heading", TimeSpan.FromSeconds(30));
|
||||
string editor = ResolveEditorProbe();
|
||||
session.InvokeMenuItem("File", "ConfigureEditorMenuItem");
|
||||
session.EnterText("EditorPath", editor);
|
||||
session.ClickButton("Save");
|
||||
session.WaitForElementGone("EditorPath");
|
||||
using(var settings = JsonDocument.Parse(session.ReadSettings()))
|
||||
{
|
||||
Assert.Equal(editor, settings.RootElement.GetProperty("ExternalEditor").GetString());
|
||||
}
|
||||
session.InvokeMenuItem("File", "OpenEditorMenuItem");
|
||||
string resultPath = Path.Combine(session.DataDirectory, "editor-result.json");
|
||||
session.WaitFor(() => File.Exists(resultPath) ? session.MainWindow : null, "the actual editor fixture's result file");
|
||||
using var result = JsonDocument.Parse(File.ReadAllText(resultPath));
|
||||
Assert.Equal(Path.Combine(session.DataDirectory, "notes & café $HOME.md"), Assert.Single(result.RootElement.GetProperty("Arguments").EnumerateArray()).GetString());
|
||||
Assert.Equal(session.DataDirectory, result.RootElement.GetProperty("WorkingDirectory").GetString());
|
||||
Assert.NotNull(session.FindInApp(cf => cf.ByName("Editor fixture")));
|
||||
Assert.DoesNotContain("notes & café", session.ReadLogs(), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>A missing choice keeps the dialog open; cancel does not change saved settings.</summary>
|
||||
[Fact]
|
||||
public void Editor_InvalidExecutableAndCancel_DoesNotPersist()
|
||||
{
|
||||
using var session = new AppSession();
|
||||
session.InvokeMenuItem("File", "ConfigureEditorMenuItem");
|
||||
session.EnterText("EditorPath", Path.Combine(session.DataDirectory, "missing.exe"));
|
||||
session.ClickButton("Save");
|
||||
session.WaitFor(() => session.FindInApp(cf => cf.ByName("Enter the full path of an existing .exe file.")), "the editor validation error");
|
||||
Assert.NotNull(session.FindInApp(cf => cf.ByAutomationId("EditorPath")));
|
||||
session.ClickButton("Cancel");
|
||||
session.WaitForElementGone("EditorPath");
|
||||
Assert.DoesNotContain("missing.exe", session.ReadSettings(), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>An editor uninstalled after configuration reports the problem without launching anything.</summary>
|
||||
[Fact]
|
||||
public void Editor_MissingConfiguredExecutable_PreservesDocument()
|
||||
{
|
||||
using var session = new AppSession(document: "# Missing editor fixture");
|
||||
session.WaitFor(() => session.FindInApp(cf => cf.ByName("Missing editor fixture")), "the fixture heading", TimeSpan.FromSeconds(30));
|
||||
string editor = Path.Combine(session.DataDirectory, "removed-editor.exe");
|
||||
File.WriteAllText(editor, "Never executed; presence-only configuration fixture.");
|
||||
session.InvokeMenuItem("File", "ConfigureEditorMenuItem");
|
||||
session.EnterText("EditorPath", editor);
|
||||
session.ClickButton("Save");
|
||||
session.WaitForElementGone("EditorPath");
|
||||
File.Delete(editor);
|
||||
session.InvokeMenuItem("File", "OpenEditorMenuItem");
|
||||
session.WaitFor(() => session.FindInApp(cf => cf.ByName("The configured editor is missing. Use File → Configure editor to choose an installed executable.")), "the missing editor status");
|
||||
Assert.NotNull(session.FindInApp(cf => cf.ByName("Missing editor fixture")));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>Uses the solution's existing test-only executable, never a real user's editor.</summary>
|
||||
private static string ResolveEditorProbe()
|
||||
{
|
||||
for(DirectoryInfo? current = new(AppContext.BaseDirectory); current is not null; current = current.Parent)
|
||||
{
|
||||
if(File.Exists(Path.Combine(current.FullName, "MarkdownViewr.Windows.slnx")))
|
||||
{
|
||||
string path = Path.Combine(current.FullName, "tests", "MarkdownViewr.Windows.UpdateProbe", "bin", "Debug", "net10.0-windows", "MarkdownViewr.Windows.UpdateProbe.exe");
|
||||
return File.Exists(path) ? path : throw new FileNotFoundException("Build the solution's test-only editor probe first.", path);
|
||||
}
|
||||
}
|
||||
throw new FileNotFoundException("Could not locate the solution's editor probe.");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Splash: shows, then dismisses itself.</summary>
|
||||
[Fact]
|
||||
public void Splash_ShowsThenDismissesItself()
|
||||
|
||||
@@ -18,6 +18,10 @@ internal static class Program
|
||||
/// <summary>Records the installed channel, update offer and requests; optionally downloads and applies it.</summary>
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
if(Environment.GetEnvironmentVariable("MARKDOWNVIEWR_EDITOR_PROBE_RESULT") is { Length: > 0 } editorResult)
|
||||
{
|
||||
return WriteEditorResult(editorResult, args);
|
||||
}
|
||||
VelopackStartup.Run();
|
||||
if(args.Length != 3 || args[2] is not ("check" or "apply"))
|
||||
{
|
||||
@@ -73,4 +77,18 @@ internal static class Program
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>Records only the owned fixture's actual process arguments; it never opens or reads the document.</summary>
|
||||
private static int WriteEditorResult(string resultPath, string[] args)
|
||||
{
|
||||
if(args.Length != 1)
|
||||
{
|
||||
throw new ArgumentException("The editor fixture expects one literal document argument.", nameof(args));
|
||||
}
|
||||
File.WriteAllText(resultPath + ".tmp", JsonSerializer.Serialize(new { Arguments = args, WorkingDirectory = Environment.CurrentDirectory }));
|
||||
File.Move(resultPath + ".tmp", resultPath, overwrite: true);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user