LayoutTest.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. using System.Collections;
  2. using QuestPDF.Drawing;
  3. using QuestPDF.Drawing.Proxy;
  4. using QuestPDF.Elements;
  5. using QuestPDF.Fluent;
  6. using QuestPDF.Helpers;
  7. using QuestPDF.Infrastructure;
  8. using SkiaSharp;
  9. namespace QuestPDF.LayoutTests.TestEngine;
  10. internal class LayoutBuilderDescriptor
  11. {
  12. public void Compose(Action<IContainer> container)
  13. {
  14. }
  15. }
  16. internal class DocumentLayoutBuilder
  17. {
  18. public List<LayoutTestResult.PageLayoutSnapshot> Commands { get; } = new();
  19. public PageLayoutDescriptor Page()
  20. {
  21. var page = new LayoutTestResult.PageLayoutSnapshot();
  22. Commands.Add(page);
  23. return new PageLayoutDescriptor(page);
  24. }
  25. }
  26. internal class PageLayoutDescriptor
  27. {
  28. private LayoutTestResult.PageLayoutSnapshot Command { get; }
  29. public PageLayoutDescriptor(LayoutTestResult.PageLayoutSnapshot command)
  30. {
  31. Command = command;
  32. }
  33. public PageLayoutDescriptor TakenAreaSize(float width, float height)
  34. {
  35. Command.RequiredArea = new Size(width, height);
  36. return this;
  37. }
  38. public PageLayoutDescriptor Content(Action<PageLayoutBuilder> content)
  39. {
  40. var pageContent = new PageLayoutBuilder();
  41. content(pageContent);
  42. Command.MockPositions = pageContent.Commands;
  43. return this;
  44. }
  45. }
  46. internal class PageLayoutBuilder
  47. {
  48. public List<LayoutTestResult.MockLayoutPosition> Commands { get;} = new();
  49. public ChildLayoutDescriptor Child(string mockId)
  50. {
  51. var child = new LayoutTestResult.MockLayoutPosition { MockId = mockId };
  52. Commands.Add(child);
  53. return new ChildLayoutDescriptor(child);
  54. }
  55. }
  56. internal class ChildLayoutDescriptor
  57. {
  58. private LayoutTestResult.MockLayoutPosition Command { get; }
  59. public ChildLayoutDescriptor(LayoutTestResult.MockLayoutPosition command)
  60. {
  61. Command = command;
  62. }
  63. public ChildLayoutDescriptor Position(float x, float y)
  64. {
  65. Command.Position = new Position(x, y);
  66. return this;
  67. }
  68. public ChildLayoutDescriptor Size(float width, float height)
  69. {
  70. Command.Size = new Size(width, height);
  71. return this;
  72. }
  73. }
  74. internal static class ElementExtensions
  75. {
  76. public static MockDescriptor Mock(this IContainer element, string id)
  77. {
  78. var mock = new ElementMock
  79. {
  80. MockId = id
  81. };
  82. element.Element(mock);
  83. return new MockDescriptor(mock);
  84. }
  85. }
  86. internal class MockDescriptor
  87. {
  88. private ElementMock Mock { get; }
  89. public MockDescriptor(ElementMock mock)
  90. {
  91. Mock = mock;
  92. }
  93. public MockDescriptor Size(float width, float height)
  94. {
  95. Mock.TotalWidth = width;
  96. Mock.TotalHeight = height;
  97. return this;
  98. }
  99. }
  100. internal class LayoutTest
  101. {
  102. private LayoutTestResult TestResult { get; } = new LayoutTestResult();
  103. public static LayoutTest HavingSpaceOfSize(float width, float height)
  104. {
  105. var result = new LayoutTest();
  106. result.TestResult.PageSize = new Size(width, height);
  107. return result;
  108. }
  109. public LayoutTest WithContent(Action<IContainer> handler)
  110. {
  111. // compose content
  112. var container = new Container();
  113. container.Element(handler);
  114. TestResult.GeneratedLayout = GenerateResult(TestResult.PageSize, container);
  115. return this;
  116. }
  117. private static ICollection<LayoutTestResult.PageLayoutSnapshot> GenerateResult(Size pageSize, Container container)
  118. {
  119. // inject dependencies
  120. var pageContext = new PageContext();
  121. pageContext.ResetPageNumber();
  122. var canvas = new PreviewerCanvas();
  123. container.InjectDependencies(pageContext, canvas);
  124. // distribute global state
  125. container.ApplyInheritedAndGlobalTexStyle(TextStyle.Default);
  126. container.ApplyContentDirection(ContentDirection.LeftToRight);
  127. container.ApplyDefaultImageConfiguration(DocumentSettings.Default.ImageRasterDpi, DocumentSettings.Default.ImageCompressionQuality, true);
  128. // render
  129. container.VisitChildren(x => (x as IStateResettable)?.ResetState());
  130. canvas.BeginDocument();
  131. var pageSizes = new List<Size>();
  132. while(true)
  133. {
  134. var spacePlan = container.Measure(pageSize);
  135. pageSizes.Add(spacePlan);
  136. if (spacePlan.Type == SpacePlanType.Wrap)
  137. {
  138. throw new Exception();
  139. }
  140. try
  141. {
  142. canvas.BeginPage(pageSize);
  143. container.Draw(pageSize);
  144. pageContext.IncrementPageNumber();
  145. }
  146. catch (Exception exception)
  147. {
  148. canvas.EndDocument();
  149. throw new Exception();
  150. }
  151. canvas.EndPage();
  152. if (spacePlan.Type == SpacePlanType.FullRender)
  153. break;
  154. }
  155. // extract results
  156. var mocks = container.ExtractElementsOfType<ElementMock>().Select(x => x.Value); // mock cannot contain another mock, flat structure
  157. return mocks
  158. .SelectMany(x => x.DrawingCommands)
  159. .GroupBy(x => x.PageNumber)
  160. .Select(x => new LayoutTestResult.PageLayoutSnapshot
  161. {
  162. RequiredArea = pageSizes[x.Key - 1],
  163. MockPositions = x
  164. .Select(y => new LayoutTestResult.MockLayoutPosition
  165. {
  166. MockId = y.MockId,
  167. Size = y.Size,
  168. Position = y.Position
  169. })
  170. .ToList()
  171. })
  172. .ToList();
  173. }
  174. public void ExpectWrap()
  175. {
  176. }
  177. public LayoutTest ExpectedDrawResult(Action<DocumentLayoutBuilder> handler)
  178. {
  179. var builder = new DocumentLayoutBuilder();
  180. handler(builder);
  181. TestResult.ExpectedLayout = builder.Commands;
  182. return this;
  183. }
  184. public void CompareVisually()
  185. {
  186. LayoutTestResultVisualization.Visualize(TestResult);
  187. }
  188. public void Validate()
  189. {
  190. if (TestResult.GeneratedLayout.Count != TestResult.ExpectedLayout.Count)
  191. throw new Exception($"Generated {TestResult.GeneratedLayout.Count} but expected {TestResult.ExpectedLayout.Count} pages.");
  192. var numberOfPages = TestResult.GeneratedLayout.Count;
  193. foreach (var i in Enumerable.Range(0, numberOfPages))
  194. {
  195. try
  196. {
  197. var actual = TestResult.GeneratedLayout.ElementAt(i);
  198. var expected = TestResult.ExpectedLayout.ElementAt(i);
  199. if (Math.Abs(actual.RequiredArea.Width - expected.RequiredArea.Width) > Size.Epsilon)
  200. throw new Exception($"Taken area width is equal to {actual.RequiredArea.Width} but expected {expected.RequiredArea.Width}");
  201. if (Math.Abs(actual.RequiredArea.Height - expected.RequiredArea.Height) > Size.Epsilon)
  202. throw new Exception($"Taken area height is equal to {actual.RequiredArea.Height} but expected {expected.RequiredArea.Height}");
  203. if (actual.MockPositions.Count != expected.MockPositions.Count)
  204. throw new Exception($"Visible {actual.MockPositions.Count} but expected {expected.MockPositions.Count}");
  205. foreach (var child in expected.MockPositions)
  206. {
  207. var matchingActualElements = actual
  208. .MockPositions
  209. .Where(x => x.MockId == child.MockId)
  210. .Where(x => Position.Equal(x.Position, child.Position))
  211. .Where(x => Size.Equal(x.Size, child.Size))
  212. .Count();
  213. if (matchingActualElements == 0)
  214. throw new Exception($"Cannot find actual drawing command for child {child.MockId} on position {child.Position} and size {child.Size}");
  215. if (matchingActualElements > 1)
  216. throw new Exception($"Found multiple drawing commands for child {child.MockId} on position {child.Position} and size {child.Size}");
  217. }
  218. // todo: add z-depth testing
  219. var actualOverlaps = GetOverlappingItems(actual.MockPositions);
  220. var expectedOverlaps = GetOverlappingItems(expected.MockPositions);
  221. foreach (var overlap in expectedOverlaps)
  222. {
  223. var matchingActualElements = actualOverlaps.Count(x => x.Item1 == overlap.Item1 && x.Item2 == overlap.Item2);
  224. if (matchingActualElements != 1)
  225. throw new Exception($"Element {overlap.Item1} should be visible underneath element {overlap.Item2}");
  226. }
  227. IEnumerable<(string, string)> GetOverlappingItems(ICollection<LayoutTestResult.MockLayoutPosition> items)
  228. {
  229. for (var i = 0; i < items.Count; i++)
  230. {
  231. for (var j = i; j < items.Count; j++)
  232. {
  233. var beforeChild = items.ElementAt(i);
  234. var afterChild = items.ElementAt(j);
  235. var beforeBoundingBox = BoundingBox.From(beforeChild.Position, beforeChild.Size);
  236. var afterBoundingBox = BoundingBox.From(afterChild.Position, afterChild.Size);
  237. var intersection = BoundingBoxExtensions.Intersection(beforeBoundingBox, afterBoundingBox);
  238. if (intersection == null)
  239. continue;
  240. yield return (beforeChild.MockId, afterChild.MockId);
  241. }
  242. }
  243. }
  244. }
  245. catch (Exception e)
  246. {
  247. throw new Exception($"Error on page {i + 1}: {e.Message}");
  248. }
  249. }
  250. }
  251. }