custom-matchers.js 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import { generateASCIITable } from './asciiTable';
  2. // http://stackoverflow.com/questions/986937/how-can-i-get-the-browsers-scrollbar-sizes
  3. const scrollbarWidth = (function calculateScrollbarWidth() {
  4. const inner = document.createElement('div');
  5. inner.style.height = '200px';
  6. inner.style.width = '100%';
  7. const outer = document.createElement('div');
  8. outer.style.boxSizing = 'content-box';
  9. outer.style.height = '150px';
  10. outer.style.left = '0px';
  11. outer.style.overflow = 'hidden';
  12. outer.style.position = 'absolute';
  13. outer.style.top = '0px';
  14. outer.style.width = '200px';
  15. outer.style.visibility = 'hidden';
  16. outer.appendChild(inner);
  17. (document.body || document.documentElement).appendChild(outer);
  18. const w1 = inner.offsetWidth;
  19. outer.style.overflow = 'scroll';
  20. let w2 = inner.offsetWidth;
  21. if (w1 === w2) {
  22. w2 = outer.clientWidth;
  23. }
  24. (document.body || document.documentElement).removeChild(outer);
  25. return (w1 - w2);
  26. }());
  27. beforeEach(function() {
  28. const currentSpec = this;
  29. function hot() {
  30. return currentSpec.$container.data('handsontable');
  31. }
  32. const matchers = {
  33. toBeInArray() {
  34. return {
  35. compare(actual, expected) {
  36. return {
  37. pass: Array.isArray(expected) && expected.indexOf(actual) > -1
  38. };
  39. }
  40. };
  41. },
  42. toBeFunction() {
  43. return {
  44. compare(actual) {
  45. return {
  46. pass: typeof actual === 'function'
  47. };
  48. }
  49. };
  50. },
  51. toBeAroundValue() {
  52. return {
  53. compare(actual, expected, diff) {
  54. const margin = diff || 1;
  55. const pass = actual >= expected - margin && actual <= expected + margin;
  56. let message = `Expected ${actual} to be around ${expected} (between ${expected - margin} and ${expected + margin})`;
  57. if (!pass) {
  58. message = `Expected ${actual} NOT to be around ${expected} (between ${expected - margin} and ${expected + margin})`;
  59. }
  60. return {
  61. pass,
  62. message
  63. };
  64. }
  65. };
  66. },
  67. /**
  68. * The matcher checks if the passed cell element is contained in the table viewport.
  69. */
  70. toBeVisibleInViewport() {
  71. return {
  72. compare(actual) {
  73. const viewport = hot().view.wt.wtTable.holder;
  74. const verticalPosition = actual.offsetTop - viewport.scrollTop + scrollbarWidth + actual.clientHeight;
  75. const horizontalPosition = actual.offsetLeft - viewport.scrollLeft + scrollbarWidth + actual.clientWidth;
  76. const pass = verticalPosition < viewport.offsetHeight && verticalPosition > 0
  77. && horizontalPosition < viewport.offsetWidth && horizontalPosition > 0;
  78. return {
  79. pass,
  80. message: 'Expected the element to be visible in the Handsontable viewport'
  81. };
  82. }
  83. };
  84. },
  85. /**
  86. * The matcher checks if the viewport is scrolled in the way that the cell is visible at the top of the viewport.
  87. */
  88. toBeVisibleAtTopOfViewport() {
  89. return {
  90. compare(actual) {
  91. const viewport = hot().view.wt.wtTable.holder;
  92. const verticalPosition = actual.offsetTop - viewport.scrollTop - 1;
  93. return {
  94. pass: verticalPosition === 0,
  95. message: 'Expected the element to be scrolled to the top of the Handsontable viewport'
  96. };
  97. }
  98. };
  99. },
  100. /**
  101. * The matcher checks if the viewport is scrolled in the way that the cell is visible at the bottom of the viewport.
  102. */
  103. toBeVisibleAtBottomOfViewport() {
  104. return {
  105. compare(actual) {
  106. const viewport = hot().view.wt.wtTable.holder;
  107. const verticalPosition = actual.offsetTop - viewport.scrollTop + scrollbarWidth + actual.clientHeight + 1;
  108. return {
  109. pass: verticalPosition === viewport.offsetHeight,
  110. message: 'Expected the element to be scrolled to the bottom of the Handsontable viewport'
  111. };
  112. }
  113. };
  114. },
  115. /**
  116. * The matcher checks if the viewport is scrolled in the way that the cell is visible on the left of the viewport.
  117. */
  118. toBeVisibleAtLeftOfViewport() {
  119. return {
  120. compare(actual) {
  121. const viewport = hot().view.wt.wtTable.holder;
  122. const horizontalPosition = viewport.scrollLeft - actual.offsetLeft;
  123. return {
  124. pass: horizontalPosition === 0,
  125. message: 'Expected the element to be scrolled to the top of the Handsontable viewport'
  126. };
  127. }
  128. };
  129. },
  130. /**
  131. * The matcher checks if the viewport is scrolled in the way that the cell is visible on the right of the viewport.
  132. */
  133. toBeVisibleAtRightOfViewport() {
  134. return {
  135. compare(actual) {
  136. const viewport = hot().view.wt.wtTable.holder;
  137. const horizontalPosition = viewport.scrollLeft - actual.offsetLeft + actual.clientWidth - scrollbarWidth + 1;
  138. return {
  139. pass: horizontalPosition === viewport.offsetWidth,
  140. message: 'Expected the element to be scrolled to the top of the Handsontable viewport'
  141. };
  142. }
  143. };
  144. },
  145. toBeListFulfillingCondition() {
  146. const redColor = '\x1b[31m';
  147. const resetColor = '\x1b[0m';
  148. return {
  149. compare(checkedArray, conditionFunction) {
  150. if (typeof conditionFunction !== 'function') {
  151. throw Error('Parameter passed to `toBeListFulfillingCondition` should be a function.');
  152. }
  153. const isListWithValues = Array.isArray(checkedArray) || checkedArray.length > 0;
  154. const elementNotFulfillingCondition = checkedArray.find(element => !conditionFunction(element));
  155. const containsUndefined = isListWithValues && checkedArray.includes(undefined);
  156. const pass = isListWithValues && !containsUndefined && elementNotFulfillingCondition === undefined;
  157. let message;
  158. if (!isListWithValues) {
  159. message = 'Non-empty list should be passed as expect parameter.';
  160. } else if (containsUndefined) {
  161. message = `List ${redColor}${checkedArray.join(', ')}${resetColor} contains ${redColor}undefined${resetColor} value.`;
  162. } else if (elementNotFulfillingCondition !== undefined) {
  163. let entityValue = elementNotFulfillingCondition;
  164. if (typeof elementNotFulfillingCondition === 'string') {
  165. entityValue = `"${elementNotFulfillingCondition}"`;
  166. }
  167. message = `Entity ${redColor}${entityValue}${resetColor}, from list: ${redColor}${checkedArray.join(', ')}${resetColor} doesn't satisfy the condition.`;
  168. }
  169. return {
  170. pass,
  171. message
  172. };
  173. }
  174. };
  175. },
  176. /**
  177. * The matcher checks if the provided selection pattern matches to the rendered cells by checking if
  178. * the appropriate CSS class name was added.
  179. *
  180. * The provided structure should be passed as an array of arrays, for instance:
  181. * ```
  182. * // Non-contiguous selection (with enabled top and left headers)
  183. * expect(`
  184. * | ║ : : * : * |
  185. * |===:===:===:===:===|
  186. * | - ║ : : A : 0 |
  187. * | - ║ : 1 : 0 : 0 |
  188. * | - ║ : 2 : 1 : 0 |
  189. * | - ║ : 2 : 1 : 0 |
  190. * | - ║ : 1 : 1 : 0 |
  191. * | - ║ : : 0 : 0 |
  192. * `).toBeMatchToSelectionPattern();
  193. * // Single cell selection (with fixedRowsTop: 1 and fixedColumnsLeft: 2)
  194. * expect(`
  195. * | : | : : |
  196. * |---:---:---:---:---|
  197. * | : | : : |
  198. * | : | : : |
  199. * | : | # : : |
  200. * | : | : : |
  201. * | : | : : |
  202. * | : | : : |
  203. * `).toBeMatchToSelectionPattern();
  204. * ```
  205. *
  206. * The meaning of the symbol used to describe the cells:
  207. * ' ' - An empty space indicates cell which doesn't have added any selection classes.
  208. * '0' - The number (from 0 to 7) indicates selected layer level.
  209. * 'A' - The letters (from A to H) indicates the position of the cell which contains the hidden editor
  210. * (which `current` class name). The letter `A` indicates the currently selected cell with
  211. * a background of the first layer and `H` as the latest layer (most dark).
  212. * '#' - The hash symbol indicates the currently selected cell without changed background color.
  213. *
  214. * The meaning of the symbol used to describe the table:
  215. * ':' - Column separator (only for better visual looks).
  216. * '║' - This symbol separates the row headers from the table content.
  217. * '===' - This symbol separates the column headers from the table content.
  218. * '|' - The symbol which indicates the left overlay edge.
  219. * '---' - The symbol which indicates the top overlay edge.
  220. */
  221. toBeMatchToSelectionPattern() {
  222. return {
  223. compare(actualPattern) {
  224. const asciiTable = generateASCIITable(hot().rootElement);
  225. const patternParts = (actualPattern || '').split(/\n/);
  226. const redundantPadding = patternParts.reduce((padding, line) => {
  227. const trimmedLine = line.trim();
  228. let nextPadding = padding;
  229. if (trimmedLine) {
  230. const currentPadding = line.search(/\S|$/);
  231. if (currentPadding < nextPadding) {
  232. nextPadding = currentPadding;
  233. }
  234. }
  235. return nextPadding;
  236. }, Infinity);
  237. const normalizedPattern = patternParts.reduce((acc, line) => {
  238. const trimmedLine = line.trim();
  239. if (trimmedLine) {
  240. acc.push(line.substr(redundantPadding));
  241. }
  242. return acc;
  243. }, []);
  244. const actualAsciiTable = normalizedPattern.join('\n');
  245. const message = `Expected the pattern selection \n${actualAsciiTable}\nto match to the visual state of the rendered selection \n${asciiTable}\n`;
  246. return {
  247. pass: asciiTable === actualAsciiTable,
  248. message,
  249. };
  250. }
  251. };
  252. },
  253. };
  254. jasmine.addMatchers(matchers);
  255. });