From 6e01ce50c9faf8f3dac6dd28ed0e600ffcc6eda9 Mon Sep 17 00:00:00 2001 From: polesye Date: Wed, 11 Dec 2013 13:13:34 +0200 Subject: [PATCH] Add unit tests. --- cms/static/coffee/spec/main.coffee | 4 + cms/static/js_test.yml | 1 + cms/static/js_test_squire.yml | 1 + common/lib/xmodule/xmodule/js/js_test.yml | 1 + .../xmodule/js/spec/capa/display_spec.coffee | 109 ++++- .../lib/xmodule/xmodule/js/spec/helper.coffee | 2 + .../xmodule/js/src/capa/display.coffee | 10 +- common/static/js/vendor/jasmine-imagediff.js | 382 ++++++++++++++++++ common/static/js_test.yml | 1 + lms/static/js_test.yml | 1 + 10 files changed, 506 insertions(+), 6 deletions(-) create mode 100644 common/static/js/vendor/jasmine-imagediff.js diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee index c84b60be61..d231e57428 100644 --- a/cms/static/coffee/spec/main.coffee +++ b/cms/static/coffee/spec/main.coffee @@ -34,6 +34,7 @@ requirejs.config({ "sinon": "xmodule_js/common_static/js/vendor/sinon-1.7.1", "squire": "xmodule_js/common_static/js/vendor/Squire", "jasmine-jquery": "xmodule_js/common_static/js/vendor/jasmine-jquery", + "jasmine-imagediff": "xmodule_js/common_static/js/vendor/jasmine-imagediff", "jasmine-stealth": "xmodule_js/common_static/js/vendor/jasmine-stealth", "jasmine.async": "xmodule_js/common_static/js/vendor/jasmine.async", "draggabilly": "xmodule_js/common_static/js/vendor/draggabilly.pkgd", @@ -151,6 +152,9 @@ requirejs.config({ "jasmine-jquery": { deps: ["jasmine"] }, + "jasmine-imagediff": { + deps: ["jasmine"] + }, "jasmine-stealth": { deps: ["jasmine"] }, diff --git a/cms/static/js_test.yml b/cms/static/js_test.yml index f33b46a25e..cdf8d9cec2 100644 --- a/cms/static/js_test.yml +++ b/cms/static/js_test.yml @@ -47,6 +47,7 @@ lib_paths: - xmodule_js/common_static/js/vendor/Squire.js - xmodule_js/common_static/js/vendor/jasmine-jquery.js - xmodule_js/common_static/js/vendor/jasmine-stealth.js + - xmodule_js/common_static/js/vendor/jasmine-imagediff.js - xmodule_js/common_static/js/vendor/jasmine.async.js - xmodule_js/common_static/js/vendor/jquery.maskedinput.min.js - xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js diff --git a/cms/static/js_test_squire.yml b/cms/static/js_test_squire.yml index 2b37e4792f..a3c7313ee8 100644 --- a/cms/static/js_test_squire.yml +++ b/cms/static/js_test_squire.yml @@ -46,6 +46,7 @@ lib_paths: - xmodule_js/common_static/js/vendor/Squire.js - xmodule_js/common_static/js/vendor/jasmine-jquery.js - xmodule_js/common_static/js/vendor/jasmine-stealth.js + - xmodule_js/common_static/js/vendor/jasmine-imagediff.js - xmodule_js/common_static/js/vendor/jasmine.async.js - xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js - xmodule_js/src/xmodule.js diff --git a/common/lib/xmodule/xmodule/js/js_test.yml b/common/lib/xmodule/xmodule/js/js_test.yml index 7fb5699ecf..0a0699df9c 100644 --- a/common/lib/xmodule/xmodule/js/js_test.yml +++ b/common/lib/xmodule/xmodule/js/js_test.yml @@ -36,6 +36,7 @@ lib_paths: - common_static/coffee/src/ajax_prefix.js - common_static/coffee/src/logger.js - common_static/js/vendor/jasmine-jquery.js + - common_static/js/vendor/jasmine-imagediff.js - common_static/js/vendor/require.js - RequireJS-namespace-undefine.js - common_static/js/vendor/jquery.min.js diff --git a/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee index 7debc16af1..45dc73824b 100644 --- a/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee @@ -303,6 +303,114 @@ describe 'Problem', -> expect($('input#1_2_1_choiceinput_2bc').attr('disabled')).not.toEqual('disabled') expect($('input#1_2_1').attr('disabled')).not.toEqual('disabled') + describe 'imageinput', -> + imageinput_html=''' +
+
+ +
+
test
+
+
+
+
+ ''' + states = [ + { + desc: 'rectangle is drawn correctly', + data: {'rectangle': '(10,10)-(30,30)'} + }, + { + desc: 'region is drawn correctly', + data: {'regions': '[[10,10],[30,30],[70,30],[20,30]]'} + }, + { + desc: 'mixed shapes are drawn correctly', + data: { + 'rectangle': '(10,10)-(30,30);(5,5)-(20,20)', + 'regions': '''[ + [[50,50],[40,40],[70,30],[50,70]], + [[90,95],[95,95],[90,70],[70,70]] + ]''' + } + }, + ] + + beforeEach -> + @problem = new Problem($('.xblock-student_view')) + @problem.el.prepend imageinput_html + + stubRequest = (data) => + spyOn($, 'postWithPrefix').andCallFake (url, callback) -> + callback answers: "1_2_1": data + + getImage = (coords, c_width, c_height) => + types = + rectangle: (coords) => + reg = /^\(([0-9]+),([0-9]+)\)-\(([0-9]+),([0-9]+)\)$/ + rects = coords.replace(/\s*/g, '').split(/;/) + + $.each rects, (index, rect) => + abs = Math.abs + points = reg.exec(rect) + if points + # width + width = abs(points[3] - points[1]) + # height + height = abs(points[4] - points[2]) + + ctx.rect(points[1], points[2], width, height) + + ctx.stroke() + ctx.fill() + + regions: (coords) => + parseCoords = (coords) => + reg = JSON.parse(coords) + + if typeof reg[0][0][0] == "undefined" + reg = [reg] + + return reg + + $.each parseCoords(coords), (index, region) => + ctx.beginPath() + $.each region, (index, point) => + if index is 0 + ctx.moveTo(point[0], point[1]) + else + ctx.lineTo(point[0], point[1]); + + ctx.closePath() + ctx.stroke() + ctx.fill() + + canvas = document.createElement('canvas') + canvas.width = c_width or 100 + canvas.height = c_height or 100 + + if canvas.getContext + ctx = canvas.getContext('2d') + else + return console.log 'Canvas is not supported.' + + ctx.fillStyle = 'rgba(255,255,255,.3)'; + ctx.strokeStyle = "#FF0000"; + ctx.lineWidth = "2"; + + $.each coords, (key, value) => + types[key](value) if types[key]? + + return canvas + + $.each states, (index, state) => + it state.desc, -> + stubRequest(state.data) + @problem.show() + img = getImage(state.data) + + expect(img).toImageDiffEqual($('canvas')[0]) + describe 'when the answers are already shown', -> beforeEach -> @problem.el.addClass 'showed' @@ -409,4 +517,3 @@ describe 'Problem', -> expect(@problem.answers).toEqual "input_1_1=one&input_1_2=two" - diff --git a/common/lib/xmodule/xmodule/js/spec/helper.coffee b/common/lib/xmodule/xmodule/js/spec/helper.coffee index 026ff1133b..851692d87a 100644 --- a/common/lib/xmodule/xmodule/js/spec/helper.coffee +++ b/common/lib/xmodule/xmodule/js/spec/helper.coffee @@ -162,6 +162,8 @@ beforeEach -> toBeInArray: (array) -> return $.inArray(@.actual, array) > -1 + @addMatchers imagediff.jasmine + # Stub jQuery.cookie $.cookie = jasmine.createSpy('jQuery.cookie').andReturn '1.0' diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index edd781f8a3..697983e299 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -506,18 +506,18 @@ class @Problem parseCoords = (coords) => reg = JSON.parse(coords) - if typeof reg[0][0] is undefined + if typeof reg[0][0][0] == "undefined" reg = [reg] return reg - $.each parseCoords(coords), (index, regions) => + $.each parseCoords(coords), (index, region) => ctx.beginPath() - $.each regions, (index, points) => + $.each region, (index, point) => if index is 0 - ctx.moveTo(points[0], points[1]) + ctx.moveTo(point[0], point[1]) else - ctx.lineTo(points[0], points[1]); + ctx.lineTo(point[0], point[1]); ctx.closePath() ctx.stroke() diff --git a/common/static/js/vendor/jasmine-imagediff.js b/common/static/js/vendor/jasmine-imagediff.js new file mode 100644 index 0000000000..6460e59dc5 --- /dev/null +++ b/common/static/js/vendor/jasmine-imagediff.js @@ -0,0 +1,382 @@ +// js-imagediff 1.0.3 +// (c) 2011-2012 Carl Sutherland, Humble Software +// Distributed under the MIT License +// For original source and documentation visit: +// http://www.github.com/HumbleSoftware/js-imagediff + +(function (name, definition) { + var root = this; + if (typeof module !== 'undefined') { + var Canvas = require('canvas'); + module.exports = definition(root, name, Canvas); + } else if (typeof define === 'function' && typeof define.amd === 'object') { + define(definition); + } else { + root[name] = definition(root, name); + } +})('imagediff', function (root, name, Canvas) { + + var + TYPE_ARRAY = /\[object Array\]/i, + TYPE_CANVAS = /\[object (Canvas|HTMLCanvasElement)\]/i, + TYPE_CONTEXT = /\[object CanvasRenderingContext2D\]/i, + TYPE_IMAGE = /\[object (Image|HTMLImageElement)\]/i, + TYPE_IMAGE_DATA = /\[object ImageData\]/i, + + UNDEFINED = 'undefined', + + canvas = getCanvas(), + context = canvas.getContext('2d'), + previous = root[name], + imagediff, jasmine; + + // Creation + function getCanvas (width, height) { + var + canvas = Canvas ? + new Canvas() : + document.createElement('canvas'); + if (width) canvas.width = width; + if (height) canvas.height = height; + return canvas; + } + function getImageData (width, height) { + canvas.width = width; + canvas.height = height; + context.clearRect(0, 0, width, height); + return context.createImageData(width, height); + } + + + // Type Checking + function isImage (object) { + return isType(object, TYPE_IMAGE); + } + function isCanvas (object) { + return isType(object, TYPE_CANVAS); + } + function isContext (object) { + return isType(object, TYPE_CONTEXT); + } + function isImageData (object) { + return !!(object && + isType(object, TYPE_IMAGE_DATA) && + typeof(object.width) !== UNDEFINED && + typeof(object.height) !== UNDEFINED && + typeof(object.data) !== UNDEFINED); + } + function isImageType (object) { + return ( + isImage(object) || + isCanvas(object) || + isContext(object) || + isImageData(object) + ); + } + function isType (object, type) { + return typeof (object) === 'object' && !!Object.prototype.toString.apply(object).match(type); + } + + + // Type Conversion + function copyImageData (imageData) { + var + height = imageData.height, + width = imageData.width, + data = imageData.data, + newImageData, newData, i; + + canvas.width = width; + canvas.height = height; + newImageData = context.getImageData(0, 0, width, height); + newData = newImageData.data; + + for (i = imageData.data.length; i--;) { + newData[i] = data[i]; + } + + return newImageData; + } + function toImageData (object) { + if (isImage(object)) { return toImageDataFromImage(object); } + if (isCanvas(object)) { return toImageDataFromCanvas(object); } + if (isContext(object)) { return toImageDataFromContext(object); } + if (isImageData(object)) { return object; } + } + function toImageDataFromImage (image) { + var + height = image.height, + width = image.width; + canvas.width = width; + canvas.height = height; + context.clearRect(0, 0, width, height); + context.drawImage(image, 0, 0); + return context.getImageData(0, 0, width, height); + } + function toImageDataFromCanvas (canvas) { + var + height = canvas.height, + width = canvas.width, + context = canvas.getContext('2d'); + return context.getImageData(0, 0, width, height); + } + function toImageDataFromContext (context) { + var + canvas = context.canvas, + height = canvas.height, + width = canvas.width; + return context.getImageData(0, 0, width, height); + } + function toCanvas (object) { + var + data = toImageData(object), + canvas = getCanvas(data.width, data.height), + context = canvas.getContext('2d'); + + context.putImageData(data, 0, 0); + return canvas; + } + + + // ImageData Equality Operators + function equalWidth (a, b) { + return a.width === b.width; + } + function equalHeight (a, b) { + return a.height === b.height; + } + function equalDimensions (a, b) { + return equalHeight(a, b) && equalWidth(a, b); + } + function equal (a, b, tolerance) { + + var + aData = a.data, + bData = b.data, + length = aData.length, + i; + + tolerance = tolerance || 0; + + if (!equalDimensions(a, b)) return false; + for (i = length; i--;) if (aData[i] !== bData[i] && Math.abs(aData[i] - bData[i]) > tolerance) return false; + + return true; + } + + + // Diff + function diff (a, b) { + return (equalDimensions(a, b) ? diffEqual : diffUnequal)(a, b); + } + function diffEqual (a, b) { + + var + height = a.height, + width = a.width, + c = getImageData(width, height), // c = a - b + aData = a.data, + bData = b.data, + cData = c.data, + length = cData.length, + row, column, + i, j, k, v; + + for (i = 0; i < length; i += 4) { + cData[i] = Math.abs(aData[i] - bData[i]); + cData[i+1] = Math.abs(aData[i+1] - bData[i+1]); + cData[i+2] = Math.abs(aData[i+2] - bData[i+2]); + cData[i+3] = Math.abs(255 - Math.abs(aData[i+3] - bData[i+3])); + } + + return c; + } + function diffUnequal (a, b) { + + var + height = Math.max(a.height, b.height), + width = Math.max(a.width, b.width), + c = getImageData(width, height), // c = a - b + aData = a.data, + bData = b.data, + cData = c.data, + rowOffset, + columnOffset, + row, column, + i, j, k, v; + + + for (i = cData.length - 1; i > 0; i = i - 4) { + cData[i] = 255; + } + + // Add First Image + offsets(a); + for (row = a.height; row--;){ + for (column = a.width; column--;) { + i = 4 * ((row + rowOffset) * width + (column + columnOffset)); + j = 4 * (row * a.width + column); + cData[i+0] = aData[j+0]; // r + cData[i+1] = aData[j+1]; // g + cData[i+2] = aData[j+2]; // b + // cData[i+3] = aData[j+3]; // a + } + } + + // Subtract Second Image + offsets(b); + for (row = b.height; row--;){ + for (column = b.width; column--;) { + i = 4 * ((row + rowOffset) * width + (column + columnOffset)); + j = 4 * (row * b.width + column); + cData[i+0] = Math.abs(cData[i+0] - bData[j+0]); // r + cData[i+1] = Math.abs(cData[i+1] - bData[j+1]); // g + cData[i+2] = Math.abs(cData[i+2] - bData[j+2]); // b + } + } + + // Helpers + function offsets (imageData) { + rowOffset = Math.floor((height - imageData.height) / 2); + columnOffset = Math.floor((width - imageData.width) / 2); + } + + return c; + } + + + // Validation + function checkType () { + var i; + for (i = 0; i < arguments.length; i++) { + if (!isImageType(arguments[i])) { + throw { + name : 'ImageTypeError', + message : 'Submitted object was not an image.' + }; + } + } + } + + + // Jasmine Matchers + function get (element, content) { + element = document.createElement(element); + if (element && content) { + element.innerHTML = content; + } + return element; + } + + jasmine = { + + toBeImageData : function () { + return imagediff.isImageData(this.actual); + }, + + toImageDiffEqual : function (expected, tolerance) { + + if (typeof (document) !== UNDEFINED) { + this.message = function () { + var + div = get('div'), + a = get('div', '
Actual:
'), + b = get('div', '
Expected:
'), + c = get('div', '
Diff:
'), + diff = imagediff.diff(this.actual, expected), + canvas = getCanvas(), + context; + + canvas.height = diff.height; + canvas.width = diff.width; + + div.style.overflow = 'hidden'; + a.style.float = 'left'; + b.style.float = 'left'; + c.style.float = 'left'; + + context = canvas.getContext('2d'); + context.putImageData(diff, 0, 0); + + a.appendChild(toCanvas(this.actual)); + b.appendChild(toCanvas(expected)); + c.appendChild(canvas); + + div.appendChild(a); + div.appendChild(b); + div.appendChild(c); + + return [ + div, + "Expected not to be equal." + ]; + }; + } + + return imagediff.equal(this.actual, expected, tolerance); + } + }; + + + // Image Output + function imageDataToPNG (imageData, outputFile, callback) { + + var + canvas = toCanvas(imageData), + base64Data, + decodedImage; + + callback = callback || Function; + + base64Data = canvas.toDataURL().replace(/^data:image\/\w+;base64,/,""); + decodedImage = new Buffer(base64Data, 'base64'); + require('fs').writeFile(outputFile, decodedImage, callback); + } + + + // Definition + imagediff = { + + createCanvas : getCanvas, + createImageData : getImageData, + + isImage : isImage, + isCanvas : isCanvas, + isContext : isContext, + isImageData : isImageData, + isImageType : isImageType, + + toImageData : function (object) { + checkType(object); + if (isImageData(object)) { return copyImageData(object); } + return toImageData(object); + }, + + equal : function (a, b, tolerance) { + checkType(a, b); + a = toImageData(a); + b = toImageData(b); + return equal(a, b, tolerance); + }, + diff : function (a, b) { + checkType(a, b); + a = toImageData(a); + b = toImageData(b); + return diff(a, b); + }, + + jasmine : jasmine, + + // Compatibility + noConflict : function () { + root[name] = previous; + return imagediff; + } + }; + + if (typeof module !== 'undefined') { + imagediff.imageDataToPNG = imageDataToPNG; + } + + return imagediff; +}); diff --git a/common/static/js_test.yml b/common/static/js_test.yml index 117fd7f662..81879153f3 100644 --- a/common/static/js_test.yml +++ b/common/static/js_test.yml @@ -30,6 +30,7 @@ prepend_path: common/static lib_paths: - js/vendor/jquery.min.js - js/vendor/jasmine-jquery.js + - js/vendor/jasmine-imagediff.js - js/vendor/underscore-min.js - js/vendor/backbone-min.js - js/vendor/jquery.timeago.js diff --git a/lms/static/js_test.yml b/lms/static/js_test.yml index a49b2fde81..a7520a8fc1 100644 --- a/lms/static/js_test.yml +++ b/lms/static/js_test.yml @@ -31,6 +31,7 @@ lib_paths: - xmodule_js/common_static/coffee/src/ajax_prefix.js - xmodule_js/common_static/coffee/src/logger.js - xmodule_js/common_static/js/vendor/jasmine-jquery.js + - xmodule_js/common_static/js/vendor/jasmine-imagediff.js - xmodule_js/common_static/js/vendor/require.js - js/RequireJS-namespace-undefine.js - xmodule_js/common_static/js/vendor/jquery.min.js