diff --git a/src/DiffEngineTray.Tests/IssueLauncherTests.cs b/src/DiffEngineTray.Tests/IssueLauncherTests.cs
new file mode 100644
index 00000000..9268b899
--- /dev/null
+++ b/src/DiffEngineTray.Tests/IssueLauncherTests.cs
@@ -0,0 +1,49 @@
+///
+/// The issue URL the tray opens after an error. Its title is built from a message that carries a
+/// file path, so it holds whatever characters the path does.
+///
+public class IssueLauncherTests
+{
+ [Test]
+ public async Task A_hash_in_the_title_does_not_start_a_fragment()
+ {
+ // Perfectly ordinary in a solution directory, and everything after it used to be read by
+ // the browser as a fragment: no body, and a title ending at the hash
+ const string message = @"Cannot start. Failed to read settings: C:\code\C#\settings.json";
+
+ var url = IssueLauncher.BuildUrl(message);
+
+ await Assert.That(url).DoesNotContain("#");
+ await Assert.That(Query(url)["title"]).IsEqualTo(message);
+ await Assert.That(Query(url)["body"]).IsNotEmpty();
+ }
+
+ [Test]
+ public async Task An_ampersand_in_the_title_does_not_truncate_it()
+ {
+ const string message = @"Could not accept 'R&D.received.txt'";
+
+ var url = IssueLauncher.BuildUrl(message);
+
+ await Assert.That(Query(url)["title"]).IsEqualTo(message);
+ await Assert.That(Query(url)["body"]).IsNotEmpty();
+ }
+
+ ///
+ /// The body is encoded by its callers and handed over already escaped, so encoding the title
+ /// must not have changed what reaches GitHub as the body.
+ ///
+ [Test]
+ public async Task Keeps_the_body_it_is_given()
+ {
+ var url = IssueLauncher.BuildUrl("TheTitle", WebUtility.UrlEncode("\n * Action: TheAction"));
+
+ await Assert.That(Query(url)["body"]).Contains("* Action: TheAction");
+ }
+
+ static Dictionary Query(string url) =>
+ url[(url.IndexOf('?') + 1)..]
+ .Split('&')
+ .Select(_ => _.Split('=', 2))
+ .ToDictionary(_ => _[0], _ => WebUtility.UrlDecode(_[1]));
+}
diff --git a/src/DiffEngineTray/IssueLauncher.cs b/src/DiffEngineTray/IssueLauncher.cs
index f09bbb10..69ca628a 100644
--- a/src/DiffEngineTray/IssueLauncher.cs
+++ b/src/DiffEngineTray/IssueLauncher.cs
@@ -11,7 +11,15 @@ static IssueLauncher() =>
""");
public static void Launch() =>
- LinkLauncher.LaunchUrl($"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/VerifyTests/DiffEngine/issues/new?title=TODO&body={defaultBody}");
+ LinkLauncher.LaunchUrl(BuildUrl("TODO"));
+
+ ///
+ /// The title is encoded like the body is. It is built from a message carrying a file path, and
+ /// a '#' in one started a fragment - dropping the body, and everything of the title after it -
+ /// while an '&' started a parameter GitHub does not have, truncating the title there.
+ ///
+ internal static string BuildUrl(string title, string extraBody = "") =>
+ $"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/VerifyTests/DiffEngine/issues/new?title={WebUtility.UrlEncode(title)}&body={defaultBody}{extraBody}";
public static void LaunchForException(string message, Exception exception)
{
@@ -43,8 +51,7 @@ Open an issue on GitHub?
{exception}
```
""");
- var url = $"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/VerifyTests/DiffEngine/issues/new?title={message}&body={defaultBody}{extraBody}";
- LinkLauncher.LaunchUrl(url);
+ LinkLauncher.LaunchUrl(BuildUrl(message, extraBody));
}
public static void LaunchForException(string message)
@@ -71,8 +78,7 @@ Open an issue on GitHub?
* Action: {message}
""");
- var url = $"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/VerifyTests/DiffEngine/issues/new?title={message}&body={defaultBody}{extraBody}";
- LinkLauncher.LaunchUrl(url);
+ LinkLauncher.LaunchUrl(BuildUrl(message, extraBody));
}
static bool AskIfOpenIssue(string text)