jasmine.matchers.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. 'use strict';
  2. var pixelmatch = require('pixelmatch');
  3. var utils = require('./jasmine.utils');
  4. function toPercent(value) {
  5. return Math.round(value * 10000) / 100;
  6. }
  7. function createImageData(w, h) {
  8. var canvas = utils.createCanvas(w, h);
  9. var context = canvas.getContext('2d');
  10. return context.getImageData(0, 0, w, h);
  11. }
  12. function canvasFromImageData(data) {
  13. var canvas = utils.createCanvas(data.width, data.height);
  14. var context = canvas.getContext('2d');
  15. context.putImageData(data, 0, 0);
  16. return canvas;
  17. }
  18. function buildPixelMatchPreview(actual, expected, diff, threshold, tolerance, count) {
  19. var ratio = count / (actual.width * actual.height);
  20. var wrapper = document.createElement('div');
  21. wrapper.style.cssText = 'display: flex; overflow-y: auto';
  22. [
  23. {data: actual, label: 'Actual'},
  24. {data: expected, label: 'Expected'},
  25. {data: diff, label:
  26. 'diff: ' + count + 'px ' +
  27. '(' + toPercent(ratio) + '%)<br/>' +
  28. 'thr: ' + toPercent(threshold) + '%, ' +
  29. 'tol: ' + toPercent(tolerance) + '%'
  30. }
  31. ].forEach(function(values) {
  32. var item = document.createElement('div');
  33. item.style.cssText = 'text-align: center; font: 12px monospace; line-height: 1.4; margin: 8px';
  34. item.innerHTML = '<div style="margin: 8px; height: 32px">' + values.label + '</div>';
  35. item.appendChild(canvasFromImageData(values.data));
  36. wrapper.appendChild(item);
  37. });
  38. // WORKAROUND: https://github.com/karma-runner/karma-jasmine/issues/139
  39. wrapper.indexOf = function() {
  40. return -1;
  41. };
  42. return wrapper;
  43. }
  44. function toBeCloseToPixel() {
  45. return {
  46. compare: function(actual, expected) {
  47. var result = false;
  48. if (!isNaN(actual) && !isNaN(expected)) {
  49. var diff = Math.abs(actual - expected);
  50. var A = Math.abs(actual);
  51. var B = Math.abs(expected);
  52. var percentDiff = 0.005; // 0.5% diff
  53. result = (diff <= (A > B ? A : B) * percentDiff) || diff < 2; // 2 pixels is fine
  54. }
  55. return {pass: result};
  56. }
  57. };
  58. }
  59. function toEqualOneOf() {
  60. return {
  61. compare: function(actual, expecteds) {
  62. var result = false;
  63. for (var i = 0, l = expecteds.length; i < l; i++) {
  64. if (actual === expecteds[i]) {
  65. result = true;
  66. break;
  67. }
  68. }
  69. return {
  70. pass: result
  71. };
  72. }
  73. };
  74. }
  75. function toBeValidChart() {
  76. return {
  77. compare: function(actual) {
  78. var message = null;
  79. if (!(actual instanceof Chart)) {
  80. message = 'Expected ' + actual + ' to be an instance of Chart';
  81. } else if (Object.prototype.toString.call(actual.canvas) !== '[object HTMLCanvasElement]') {
  82. message = 'Expected canvas to be an instance of HTMLCanvasElement';
  83. } else if (Object.prototype.toString.call(actual.ctx) !== '[object CanvasRenderingContext2D]') {
  84. message = 'Expected context to be an instance of CanvasRenderingContext2D';
  85. } else if (typeof actual.height !== 'number' || !isFinite(actual.height)) {
  86. message = 'Expected height to be a strict finite number';
  87. } else if (typeof actual.width !== 'number' || !isFinite(actual.width)) {
  88. message = 'Expected width to be a strict finite number';
  89. }
  90. return {
  91. message: message ? message : 'Expected ' + actual + ' to be valid chart',
  92. pass: !message
  93. };
  94. }
  95. };
  96. }
  97. function toBeChartOfSize() {
  98. return {
  99. compare: function(actual, expected) {
  100. var res = toBeValidChart().compare(actual);
  101. if (!res.pass) {
  102. return res;
  103. }
  104. var message = null;
  105. var canvas = actual.ctx.canvas;
  106. var style = getComputedStyle(canvas);
  107. var pixelRatio = actual.options.devicePixelRatio || window.devicePixelRatio;
  108. var dh = parseInt(style.height, 10) || 0;
  109. var dw = parseInt(style.width, 10) || 0;
  110. var rh = canvas.height;
  111. var rw = canvas.width;
  112. var orh = rh / pixelRatio;
  113. var orw = rw / pixelRatio;
  114. // sanity checks
  115. if (actual.height !== orh) {
  116. message = 'Expected chart height ' + actual.height + ' to be equal to original render height ' + orh;
  117. } else if (actual.width !== orw) {
  118. message = 'Expected chart width ' + actual.width + ' to be equal to original render width ' + orw;
  119. }
  120. // validity checks
  121. if (dh !== expected.dh) {
  122. message = 'Expected display height ' + dh + ' to be equal to ' + expected.dh;
  123. } else if (dw !== expected.dw) {
  124. message = 'Expected display width ' + dw + ' to be equal to ' + expected.dw;
  125. } else if (rh !== expected.rh) {
  126. message = 'Expected render height ' + rh + ' to be equal to ' + expected.rh;
  127. } else if (rw !== expected.rw) {
  128. message = 'Expected render width ' + rw + ' to be equal to ' + expected.rw;
  129. }
  130. return {
  131. message: message ? message : 'Expected ' + actual + ' to be a chart of size ' + expected,
  132. pass: !message
  133. };
  134. }
  135. };
  136. }
  137. function toEqualImageData() {
  138. return {
  139. compare: function(actual, expected, opts) {
  140. var message = null;
  141. var debug = opts.debug || false;
  142. var tolerance = opts.tolerance === undefined ? 0.001 : opts.tolerance;
  143. var threshold = opts.threshold === undefined ? 0.1 : opts.threshold;
  144. var ctx, idata, ddata, w, h, count, ratio;
  145. if (actual instanceof Chart) {
  146. ctx = actual.ctx;
  147. } else if (actual instanceof HTMLCanvasElement) {
  148. ctx = actual.getContext('2d');
  149. } else if (actual instanceof CanvasRenderingContext2D) {
  150. ctx = actual;
  151. }
  152. if (ctx) {
  153. h = expected.height;
  154. w = expected.width;
  155. idata = ctx.getImageData(0, 0, w, h);
  156. ddata = createImageData(w, h);
  157. count = pixelmatch(idata.data, expected.data, ddata.data, w, h, {threshold: threshold});
  158. ratio = count / (w * h);
  159. if ((ratio > tolerance) || debug) {
  160. message = buildPixelMatchPreview(idata, expected, ddata, threshold, tolerance, count);
  161. }
  162. } else {
  163. message = 'Input value is not a valid image source.';
  164. }
  165. return {
  166. message: message,
  167. pass: !message
  168. };
  169. }
  170. };
  171. }
  172. module.exports = {
  173. toBeCloseToPixel: toBeCloseToPixel,
  174. toEqualOneOf: toEqualOneOf,
  175. toBeValidChart: toBeValidChart,
  176. toBeChartOfSize: toBeChartOfSize,
  177. toEqualImageData: toEqualImageData
  178. };