LayoutTest.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  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 = LayoutTestExecutor.Execute(TestResult.PageSize, container);
  115. return this;
  116. }
  117. public void ExpectWrap()
  118. {
  119. }
  120. public LayoutTest ExpectedDrawResult(Action<DocumentLayoutBuilder> handler)
  121. {
  122. var builder = new DocumentLayoutBuilder();
  123. handler(builder);
  124. TestResult.ExpectedLayout = builder.Commands;
  125. return this;
  126. }
  127. public void CompareVisually()
  128. {
  129. var path = "output.pdf";
  130. if (File.Exists(path))
  131. File.Delete(path);
  132. var stream = new FileStream(path, FileMode.CreateNew);
  133. LayoutTestResultVisualization.Visualize(TestResult, stream);
  134. stream.Dispose();
  135. GenerateExtensions.OpenFileUsingDefaultProgram(path);
  136. }
  137. public void Validate()
  138. {
  139. if (TestResult.GeneratedLayout.Count != TestResult.ExpectedLayout.Count)
  140. throw new Exception($"Generated {TestResult.GeneratedLayout.Count} but expected {TestResult.ExpectedLayout.Count} pages.");
  141. var numberOfPages = TestResult.GeneratedLayout.Count;
  142. foreach (var i in Enumerable.Range(0, numberOfPages))
  143. {
  144. try
  145. {
  146. var actual = TestResult.GeneratedLayout.ElementAt(i);
  147. var expected = TestResult.ExpectedLayout.ElementAt(i);
  148. if (Math.Abs(actual.RequiredArea.Width - expected.RequiredArea.Width) > Size.Epsilon)
  149. throw new Exception($"Taken area width is equal to {actual.RequiredArea.Width} but expected {expected.RequiredArea.Width}");
  150. if (Math.Abs(actual.RequiredArea.Height - expected.RequiredArea.Height) > Size.Epsilon)
  151. throw new Exception($"Taken area height is equal to {actual.RequiredArea.Height} but expected {expected.RequiredArea.Height}");
  152. if (actual.MockPositions.Count != expected.MockPositions.Count)
  153. throw new Exception($"Visible {actual.MockPositions.Count} but expected {expected.MockPositions.Count}");
  154. foreach (var child in expected.MockPositions)
  155. {
  156. var matchingActualElements = actual
  157. .MockPositions
  158. .Where(x => x.MockId == child.MockId)
  159. .Where(x => Position.Equal(x.Position, child.Position))
  160. .Where(x => Size.Equal(x.Size, child.Size))
  161. .Count();
  162. if (matchingActualElements == 0)
  163. throw new Exception($"Cannot find actual drawing command for child {child.MockId} on position {child.Position} and size {child.Size}");
  164. if (matchingActualElements > 1)
  165. throw new Exception($"Found multiple drawing commands for child {child.MockId} on position {child.Position} and size {child.Size}");
  166. }
  167. // todo: add z-depth testing
  168. var actualOverlaps = GetOverlappingItems(actual.MockPositions);
  169. var expectedOverlaps = GetOverlappingItems(expected.MockPositions);
  170. foreach (var overlap in expectedOverlaps)
  171. {
  172. var matchingActualElements = actualOverlaps.Count(x => x.Item1 == overlap.Item1 && x.Item2 == overlap.Item2);
  173. if (matchingActualElements != 1)
  174. throw new Exception($"Element {overlap.Item1} should be visible underneath element {overlap.Item2}");
  175. }
  176. IEnumerable<(string, string)> GetOverlappingItems(ICollection<LayoutTestResult.MockLayoutPosition> items)
  177. {
  178. for (var i = 0; i < items.Count; i++)
  179. {
  180. for (var j = i; j < items.Count; j++)
  181. {
  182. var beforeChild = items.ElementAt(i);
  183. var afterChild = items.ElementAt(j);
  184. var beforeBoundingBox = BoundingBox.From(beforeChild.Position, beforeChild.Size);
  185. var afterBoundingBox = BoundingBox.From(afterChild.Position, afterChild.Size);
  186. var intersection = BoundingBoxExtensions.Intersection(beforeBoundingBox, afterBoundingBox);
  187. if (intersection == null)
  188. continue;
  189. yield return (beforeChild.MockId, afterChild.MockId);
  190. }
  191. }
  192. }
  193. }
  194. catch (Exception e)
  195. {
  196. throw new Exception($"Error on page {i + 1}: {e.Message}");
  197. }
  198. }
  199. }
  200. }