diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/UpdatesNotification.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/UpdatesNotification.cs index eb4557c04d2..77fbc426357 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/UpdatesNotification.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/UpdatesNotification.cs @@ -111,36 +111,52 @@ internal static void ShowUpdateNotification(PSHostUserInterface hostUI) ? ManagedEntranceStrings.StableUpdateNotificationMessage : ManagedEntranceStrings.PreviewUpdateNotificationMessage; - string notificationColor = string.Empty; - string resetColor = string.Empty; + string notificationMsg = FormatUpdateNotification(notificationMsgTemplate, releaseTag, hostUI.SupportsVirtualTerminal); - string line2Padding = string.Empty; - string line3Padding = string.Empty; + hostUI.WriteLine(); + hostUI.WriteLine(notificationMsg); + } + } - // We calculate how much whitespace we need to make it look nice - if (hostUI.SupportsVirtualTerminal) - { - // Swaps foreground and background colors. - notificationColor = "\x1B[7m"; - resetColor = "\x1B[0m"; - - // The first line is longest, if the message changes, this needs to be updated - int line1Length = notificationMsgTemplate.IndexOf('\n'); - int line2Length = notificationMsgTemplate.IndexOf('\n', line1Length + 1); - int line3Length = notificationMsgTemplate.IndexOf('\n', line2Length + 1); - line3Length -= line2Length + 1; - line2Length -= line1Length + 1; - - line2Padding = line2Padding.PadRight(line1Length - line2Length + releaseTag.Length); - // 3 represents the extra placeholder in the template - line3Padding = line3Padding.PadRight(line1Length - line3Length + 3); - } + /// + /// Formats the update notification message from the given template. + /// + /// The three-line notification message template. + /// The version of the new release. + /// Whether to highlight the message and pad the shorter lines to the width of the first line. + /// The formatted notification message. + internal static string FormatUpdateNotification(string notificationMsgTemplate, string releaseTag, bool supportsVirtualTerminal) + { + string notificationColor = string.Empty; + string resetColor = string.Empty; - string notificationMsg = string.Format(CultureInfo.CurrentCulture, notificationMsgTemplate, releaseTag, notificationColor, resetColor, line2Padding, line3Padding); + string line2Padding = string.Empty; + string line3Padding = string.Empty; - hostUI.WriteLine(); - hostUI.WriteLine(notificationMsg); + // We calculate how much whitespace we need to make it look nice + if (supportsVirtualTerminal) + { + // Swaps foreground and background colors. + notificationColor = "\x1B[7m"; + resetColor = "\x1B[0m"; + + // The first line is usually the longest, but localized templates may have a longer + // second or third line. Clamp the padding so it can't go negative and crash the host + // over a cosmetic alignment mismatch. + int line1Length = notificationMsgTemplate.IndexOf('\n'); + int line2Length = notificationMsgTemplate.IndexOf('\n', line1Length + 1); + int line3Length = notificationMsgTemplate.IndexOf('\n', line2Length + 1); + line3Length -= line2Length + 1; + line2Length -= line1Length + 1; + + line2Padding = line2Padding.PadRight( + Math.Max(0, line1Length - line2Length + releaseTag.Length)); + // 3 represents the extra placeholder in the template + line3Padding = line3Padding.PadRight( + Math.Max(0, line1Length - line3Length + 3)); } + + return string.Format(CultureInfo.CurrentCulture, notificationMsgTemplate, releaseTag, notificationColor, resetColor, line2Padding, line3Padding); } internal static async Task CheckForUpdates() diff --git a/test/xUnit/csharp/UpdatesNotificationTests.cs b/test/xUnit/csharp/UpdatesNotificationTests.cs new file mode 100644 index 00000000000..3c1f5b49e07 --- /dev/null +++ b/test/xUnit/csharp/UpdatesNotificationTests.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Globalization; + +using Microsoft.PowerShell; +using Xunit; + +namespace PSTests.Parallel +{ + public static class UpdatesNotificationTests + { + private const string ReleaseTag = "7.6.7"; + + private static readonly string[] TemplateNames = new[] + { + nameof(ManagedEntranceStrings.StableUpdateNotificationMessage), + nameof(ManagedEntranceStrings.LTSUpdateNotificationMessage), + nameof(ManagedEntranceStrings.PreviewUpdateNotificationMessage), + }; + + // Keep in sync with the localized resources under src/Microsoft.PowerShell.ConsoleHost/resources. + private static readonly string[] LocalizedCultures = new[] + { + "cs", "de", "es", "fr", "it", "ja", "ko", "pl", "pt-BR", "ru", "tr", "zh-Hans", "zh-Hant", + }; + + public static IEnumerable NeutralTemplates() + { + foreach (string templateName in TemplateNames) + { + yield return new object[] { templateName }; + } + } + + public static IEnumerable LocalizedTemplates() + { + foreach (string cultureName in LocalizedCultures) + { + foreach (string templateName in TemplateNames) + { + yield return new object[] { cultureName, templateName }; + } + } + } + + [Theory] + [MemberData(nameof(NeutralTemplates))] + public static void TestNeutralTemplateLinesHaveSameWidth(string templateName) + { + string template = ManagedEntranceStrings.ResourceManager.GetString(templateName, CultureInfo.InvariantCulture); + + string message = UpdatesNotification.FormatUpdateNotification(template, ReleaseTag, supportsVirtualTerminal: true); + + string[] lines = message.Split('\n'); + Assert.Equal(lines[0].Length, lines[1].Length); + Assert.Equal(lines[0].Length, lines[2].Length); + } + + [Theory] + [InlineData(" {1} Short {0} {2}\n {1} A second line that is longer than the first one:{3}{2}\n {1} https://aka.ms/PowerShell-Release?tag=v{0} {4}{2}\n")] + [InlineData(" {1} New release: v{0} {2}\n {1} Upgrade now:{3}{2}\n {1} https://aka.ms/PowerShell-Release?tag=v{0} {4}{2}\n")] + public static void TestTemplateWithLongerLaterLine(string template) + { + string message = UpdatesNotification.FormatUpdateNotification(template, ReleaseTag, supportsVirtualTerminal: true); + + Assert.Contains($"tag=v{ReleaseTag}", message); + } + + [SkippableTheory] + [MemberData(nameof(LocalizedTemplates))] + public static void TestLocalizedTemplate(string cultureName, string templateName) + { + string template = ManagedEntranceStrings.ResourceManager.GetString(templateName, new CultureInfo(cultureName)); + string neutralTemplate = ManagedEntranceStrings.ResourceManager.GetString(templateName, CultureInfo.InvariantCulture); + Skip.If(template == neutralTemplate, $"The '{cultureName}' resources are not available."); + + string message = UpdatesNotification.FormatUpdateNotification(template, ReleaseTag, supportsVirtualTerminal: true); + + Assert.Contains($"tag=v{ReleaseTag}", message); + } + } +}