PageRenderTime 47ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/UnitTests/GitUI.Tests/Theming/ThemeLoaderTests.cs

https://github.com/gitextensions/gitextensions
C# | 277 lines | 228 code | 49 blank | 0 comment | 0 complexity | 667ba7cc6ccdc04c267a8214eff9ccb2 MD5 | raw file
Possible License(s): GPL-3.0
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.IO;
  5. using System.Linq;
  6. using FluentAssertions;
  7. using GitExtUtils.GitUI.Theming;
  8. using GitUI.Theming;
  9. using NSubstitute;
  10. using NUnit.Framework;
  11. namespace GitUITests.Theming
  12. {
  13. [TestFixture]
  14. public class ThemeLoaderTests
  15. {
  16. private static readonly IReadOnlyCollection<KnownColor> ThemableSystemColorNames =
  17. Theme.TestAccessor.SysColorNames;
  18. private static readonly IReadOnlyCollection<Color> TestColorValues =
  19. new[]
  20. {
  21. Color.Red,
  22. Color.Green,
  23. };
  24. private static readonly IReadOnlyCollection<Color> AlternativeTestColorValues =
  25. new[]
  26. {
  27. Color.LightCoral,
  28. Color.LightGreen,
  29. };
  30. private static readonly IReadOnlyCollection<AppColor> AppColorNames =
  31. Enum.GetValues(typeof(AppColor))
  32. .Cast<AppColor>()
  33. .ToList();
  34. private const string MockAppThemesDirectory = "c:\\gitextensions\\themes";
  35. private const string MockUserThemesDirectory = "c:\\appdata\\gitextensions\\themes";
  36. #if SUPPORT_THEMES
  37. [Test]
  38. public void Should_load_any_themable_system_color(
  39. [ValueSource(nameof(ThemableSystemColorNames))] KnownColor colorName,
  40. [ValueSource(nameof(TestColorValues))] Color testColorValue)
  41. {
  42. var mockFileReader = CreateMockFileReader(GetThemeContent(colorName, testColorValue));
  43. var mockCssUrlResolver = Substitute.For<IThemeCssUrlResolver>();
  44. ThemeLoader loader = new(mockCssUrlResolver, mockFileReader);
  45. var theme = LoadTheme(loader);
  46. theme.GetColor(colorName).ToArgb().Should().Be(testColorValue.ToArgb());
  47. }
  48. [Test]
  49. public void Should_load_any_app_color(
  50. [ValueSource(nameof(AppColorNames))] AppColor colorName,
  51. [ValueSource(nameof(TestColorValues))] Color color)
  52. {
  53. var mockFileReader = CreateMockFileReader(GetThemeContent(colorName, color));
  54. var mockCssUrlResolver = Substitute.For<IThemeCssUrlResolver>();
  55. ThemeLoader loader = new(mockCssUrlResolver, mockFileReader);
  56. var theme = LoadTheme(loader);
  57. theme.GetColor(colorName).ToArgb().Should().Be(color.ToArgb());
  58. }
  59. [Test]
  60. public void Should_apply_colorblind_overrides_to_system_colors(
  61. [ValueSource(nameof(ThemableSystemColorNames))] KnownColor colorName)
  62. {
  63. var regularColor = Color.Red;
  64. var colorblindColor = Color.Blue;
  65. var mockFileReader = CreateMockFileReader(GetThemeContent(colorName, regularColor, colorblindColor));
  66. var mockCssUrlResolver = Substitute.For<IThemeCssUrlResolver>();
  67. ThemeLoader loader = new(mockCssUrlResolver, mockFileReader);
  68. var regularTheme = LoadTheme(loader);
  69. regularTheme.GetColor(colorName).ToArgb().Should().Be(regularColor.ToArgb());
  70. var colorblindTheme = LoadTheme(loader, ThemeVariations.Colorblind);
  71. colorblindTheme.GetColor(colorName).ToArgb().Should().Be(colorblindColor.ToArgb());
  72. }
  73. [Test]
  74. public void Should_apply_colorblind_overrides_to_app_colors(
  75. [ValueSource(nameof(AppColorNames))] AppColor colorName)
  76. {
  77. var regularColor = Color.Red;
  78. var colorblindColor = Color.Blue;
  79. var mockFileReader = CreateMockFileReader(GetThemeContent(colorName, regularColor, colorblindColor));
  80. var mockCssUrlResolver = Substitute.For<IThemeCssUrlResolver>();
  81. ThemeLoader loader = new(mockCssUrlResolver, mockFileReader);
  82. var regularTheme = LoadTheme(loader);
  83. regularTheme.GetColor(colorName).ToArgb().Should().Be(regularColor.ToArgb());
  84. var colorblindTheme = LoadTheme(loader, ThemeVariations.Colorblind);
  85. colorblindTheme.GetColor(colorName).ToArgb().Should().Be(colorblindColor.ToArgb());
  86. }
  87. [Test]
  88. public void Should_tolerate_css_comment(
  89. [Values(KnownColor.Control)] KnownColor colorName,
  90. [ValueSource(nameof(TestColorValues))] Color testColorValue)
  91. {
  92. string commentedContent =
  93. "/* entire first line with comment */" + Environment.NewLine +
  94. GetThemeContent(colorName, testColorValue) + "/* comment after definition */";
  95. var mockFileReader = CreateMockFileReader(commentedContent);
  96. var mockCssUrlResolver = Substitute.For<IThemeCssUrlResolver>();
  97. ThemeLoader loader = new(mockCssUrlResolver, mockFileReader);
  98. var theme = LoadTheme(loader);
  99. theme.GetColor(colorName).ToArgb().Should().Be(testColorValue.ToArgb());
  100. }
  101. [Test]
  102. public void Should_throw_When_unknown_color_name()
  103. {
  104. var mockFileReader = CreateMockFileReader(GetThemeContent("InvalidColorName", Color.Red));
  105. var mockCssUrlResolver = Substitute.For<IThemeCssUrlResolver>();
  106. ThemeLoader loader = new(mockCssUrlResolver, mockFileReader);
  107. loader.Invoking(l => LoadTheme(l))
  108. .Should().Throw<ThemeException>()
  109. .Which.Message.Should().Contain("InvalidColorName");
  110. }
  111. [Test]
  112. public void Should_throw_When_css_syntax_error()
  113. {
  114. var mockFileReader = CreateMockFileReader(GetThemeContent(KnownColor.Control, Color.Red) + "}");
  115. var mockCssUrlResolver = Substitute.For<IThemeCssUrlResolver>();
  116. ThemeLoader loader = new(mockCssUrlResolver, mockFileReader);
  117. loader.Invoking(l => LoadTheme(l))
  118. .Should().Throw<ThemeException>()
  119. .Which.Message.Should().Contain("Error parsing CSS");
  120. }
  121. [Test]
  122. public void Should_honor_css_import_directive(
  123. [Values(KnownColor.Control)] KnownColor colorName,
  124. [ValueSource(nameof(TestColorValues))] Color baseColor)
  125. {
  126. var pathProvider = CreateMockPathProvider();
  127. ThemeCssUrlResolver resolver = new(pathProvider);
  128. string themePath = Path.Combine(pathProvider.AppThemesDirectory, "theme.css");
  129. string baseThemePath = Path.Combine(pathProvider.AppThemesDirectory, "base.css");
  130. var mockFileReader = CreateMockFileReader(new Dictionary<string, string>
  131. {
  132. [baseThemePath] = GetThemeContent(colorName, baseColor),
  133. [themePath] = "@import url(\"base.css\");",
  134. });
  135. ThemeLoader loader = new(resolver, mockFileReader);
  136. var theme = loader.LoadTheme(themePath, new ThemeId("theme", isBuiltin: true), allowedClasses: ThemeVariations.None);
  137. theme.GetColor(colorName).ToArgb().Should().Be(baseColor.ToArgb());
  138. }
  139. [Test]
  140. public void Should_override_imported_color_When_current_theme_redefines_it(
  141. [Values(KnownColor.Control)] KnownColor colorName,
  142. [ValueSource(nameof(TestColorValues))] Color baseColor,
  143. [ValueSource(nameof(AlternativeTestColorValues))] Color colorOverride)
  144. {
  145. var pathProvider = CreateMockPathProvider();
  146. ThemeCssUrlResolver resolver = new(pathProvider);
  147. string themePath = Path.Combine(pathProvider.AppThemesDirectory, "theme.css");
  148. string baseThemePath = Path.Combine(pathProvider.AppThemesDirectory, "base.css");
  149. var mockFileReader = CreateMockFileReader(new Dictionary<string, string>
  150. {
  151. [baseThemePath] = GetThemeContent(colorName, baseColor),
  152. [themePath] =
  153. "@import url(\"base.css\");" + Environment.NewLine +
  154. GetThemeContent(colorName, colorOverride)
  155. });
  156. ThemeLoader loader = new(resolver, mockFileReader);
  157. var theme = loader.LoadTheme(themePath, new ThemeId("theme", isBuiltin: true), allowedClasses: ThemeVariations.None);
  158. theme.GetColor(colorName).ToArgb().Should().Be(colorOverride.ToArgb());
  159. }
  160. #endif
  161. [Test]
  162. public void Should_throw_When_cyclic_css_imports()
  163. {
  164. var pathProvider = CreateMockPathProvider();
  165. ThemeCssUrlResolver resolver = new(pathProvider);
  166. string themePath = Path.Combine(pathProvider.AppThemesDirectory, "theme.css");
  167. string baseThemePath = Path.Combine(pathProvider.AppThemesDirectory, "base.css");
  168. var mockFileReader = CreateMockFileReader(new Dictionary<string, string>
  169. {
  170. [baseThemePath] = "@import url(\"theme.css\");",
  171. [themePath] = "@import url(\"base.css\");"
  172. });
  173. ThemeLoader loader = new(resolver, mockFileReader);
  174. loader.Invoking(_ => _.LoadTheme(
  175. themePath,
  176. new ThemeId("theme", isBuiltin: true),
  177. allowedClasses: ThemeVariations.None))
  178. .Should().Throw<ThemeException>()
  179. .Which.Message.Should().Contain("Cycling CSS import");
  180. }
  181. private static IThemePathProvider CreateMockPathProvider()
  182. {
  183. var provider = Substitute.For<IThemePathProvider>();
  184. provider.ThemeExtension.Returns(".css");
  185. provider.AppThemesDirectory.Returns(MockAppThemesDirectory);
  186. provider.UserThemesDirectory.Returns(MockUserThemesDirectory);
  187. provider.GetThemePath(Arg.Any<ThemeId>())
  188. .Returns(callInfo =>
  189. Path.Combine(
  190. callInfo.Arg<ThemeId>().IsBuiltin
  191. ? MockAppThemesDirectory
  192. : MockUserThemesDirectory,
  193. callInfo.Arg<ThemeId>().Name + ".css"));
  194. return provider;
  195. }
  196. private static IThemeFileReader CreateMockFileReader(string content)
  197. {
  198. var reader = Substitute.For<IThemeFileReader>();
  199. reader.ReadThemeFile(Arg.Any<string>()).Returns(content);
  200. return reader;
  201. }
  202. private static IThemeFileReader CreateMockFileReader(IReadOnlyDictionary<string, string> contentByFile)
  203. {
  204. var reader = Substitute.For<IThemeFileReader>();
  205. reader.ReadThemeFile(Arg.Any<string>()).Returns(callInfo => contentByFile[callInfo.Arg<string>()]);
  206. return reader;
  207. }
  208. private static string GetThemeContent(object colorName, Color regularColor, Color colorblindColor) =>
  209. GetThemeContent(
  210. new Dictionary<object, Color>
  211. {
  212. [colorName] = regularColor,
  213. [colorName + ".colorblind"] = colorblindColor
  214. });
  215. private static string GetThemeContent(object colorName, Color color) =>
  216. GetThemeContent(new Dictionary<object, Color> { [colorName] = color });
  217. private static string GetThemeContent(IEnumerable<KeyValuePair<object, Color>> colorByName) =>
  218. string.Join(
  219. Environment.NewLine,
  220. colorByName.Select(
  221. pair => $".{pair.Key}: {{ color: {ThemePersistence.TestAccessor.FormatColor(pair.Value)}; }}"));
  222. private static Theme LoadTheme(ThemeLoader loader, params string[] variations) =>
  223. loader.LoadTheme(
  224. "arbitrary\\theme-path.css",
  225. new ThemeId("arbitrary_theme_name", isBuiltin: true),
  226. allowedClasses: variations);
  227. }
  228. }