diff --git a/edx_sga/static/js/spec/main.js b/edx_sga/static/js/spec/main.js new file mode 100644 index 00000000..744e2b10 --- /dev/null +++ b/edx_sga/static/js/spec/main.js @@ -0,0 +1,30 @@ +(function(requirejs, define) { + requirejs.config({ + paths: { + 'jquery': 'js/bower/jquery/dist/jquery', + 'sinon': 'js/bower/sinon/lib/sinon', + 'URI': 'js/bower/URIjs/src/URI', + 'underscore': 'js/bower/underscore/underscore', + 'IPv6': 'js/bower/URIjs/src/IPv6', + 'punycode': 'js/bower/URIjs/src/punycode', + 'SecondLevelDomains': 'js/bower/URIjs/src/SecondLevelDomains' + }, + shim: { + 'underscore': { + exports: "_" + }, + 'sinon': { + exports: "sinon" + }, + 'URI': { + exports: 'URI', + deps: ['sinon'] + } + } + }); + + define("main", [ + 'js/spec/test_studio' + ]); + +})(requirejs, define); diff --git a/edx_sga/static/js/spec/test_studio.js b/edx_sga/static/js/spec/test_studio.js new file mode 100644 index 00000000..effe20cd --- /dev/null +++ b/edx_sga/static/js/spec/test_studio.js @@ -0,0 +1,52 @@ +define(["js/spec_helpers/ajax_helpers", "js/src/studio"], function(AjaxHelpers) { + 'use strict'; + describe("studio.js", function() { + describe("StaffGradedAssignmentXBlock", function() { + it("saves the view", function() { + // Spy on Ajax requests + var requests = AjaxHelpers.requests(this); + + // Mock some arguments + var fakeUrl = "/test_url/"; + var element = $("
" + + "" + + "" + + "
"); + var server = null; + + var runtime = { + handlerUrl: function() { + return fakeUrl; + }, + notify: function(type, state) { + notifyStates[type] = state; + } + }; + + var XBlock = StaffGradedAssignmentXBlock(runtime, element, server); + // Function expects this.runtime to exist + XBlock.save = XBlock.save.bind({runtime: runtime}); + + var notifyStates = {}; + + // Execute the save + XBlock.save(); + + // Verify the request was made + AjaxHelpers.expectRequest( + requests, 'POST', fakeUrl, JSON.stringify({ + one: "1", + two: "2", + three: "3" + }) + ); + + // Complete ajax request + AjaxHelpers.respondWithJson(requests, {}); + + expect(notifyStates.save.state).toBe('end'); + }); + + }); + }); +}); diff --git a/edx_sga/static/js/spec_helpers/ajax_helpers.js b/edx_sga/static/js/spec_helpers/ajax_helpers.js new file mode 100644 index 00000000..8545bfd4 --- /dev/null +++ b/edx_sga/static/js/spec_helpers/ajax_helpers.js @@ -0,0 +1,169 @@ +// NOTE: copied from https://github.com/edx/edx-platform/blob/master/common/static/common/js/spec_helpers/ajax_helpers.js +define(['sinon', 'underscore', 'URI'], function(sinon, _, URI) { + 'use strict'; + + var fakeServer, fakeRequests, expectRequest, expectJsonRequest, expectPostRequest, expectJsonRequestURL, + respondWithJson, respondWithError, respondWithTextError, respondWithNoContent; + + /* These utility methods are used by Jasmine tests to create a mock server or + * get reference to mock requests. In either case, the cleanup (restore) is done with + * an after function. + * + * This pattern is being used instead of the more common beforeEach/afterEach pattern + * because we were seeing sporadic failures in the afterEach restore call. The cause of the + * errors were that one test suite was incorrectly being linked as the parent of an unrelated + * test suite (causing both suites' afterEach methods to be called). No solution for the root + * cause has been found, but initializing sinon and cleaning it up on a method-by-method + * basis seems to work. For more details, see STUD-1264. + */ + + /** + * Get a reference to the mocked server, and respond + * to all requests with the specified statusCode. + */ + fakeServer = function (that, response) { + var server = sinon.fakeServer.create(); + that.after(function() { + server.restore(); + }); + server.respondWith(response); + return server; + }; + + /** + * Keep track of all requests to a fake server, and + * return a reference to the Array. This allows tests + * to respond for individual requests. + */ + fakeRequests = function (that) { + console.log(sinon); + var requests = [], + xhr = sinon.useFakeXMLHttpRequest(); + xhr.onCreate = function(request) { + requests.push(request); + }; + + that.after(function() { + xhr.restore(); + }); + + return requests; + }; + + expectRequest = function(requests, method, url, body, requestIndex) { + var request; + if (_.isUndefined(requestIndex)) { + requestIndex = requests.length - 1; + } + request = requests[requestIndex]; + expect(request.url).toEqual(url); + expect(request.method).toEqual(method); + expect(request.requestBody).toEqual(body); + }; + + expectJsonRequest = function(requests, method, url, jsonRequest, requestIndex) { + var request; + if (_.isUndefined(requestIndex)) { + requestIndex = requests.length - 1; + } + request = requests[requestIndex]; + expect(request.url).toEqual(url); + expect(request.method).toEqual(method); + expect(JSON.parse(request.requestBody)).toEqual(jsonRequest); + }; + + /** + * Expect that a JSON request be made with the given URL and parameters. + * @param requests The collected requests + * @param expectedUrl The expected URL excluding the parameters + * @param expectedParameters An object representing the URL parameters + * @param requestIndex An optional index for the request (by default, the last request is used) + */ + expectJsonRequestURL = function(requests, expectedUrl, expectedParameters, requestIndex) { + var request, parameters; + if (_.isUndefined(requestIndex)) { + requestIndex = requests.length - 1; + } + request = requests[requestIndex]; + expect(new URI(request.url).path()).toEqual(expectedUrl); + parameters = new URI(request.url).query(true); + delete parameters._; // Ignore the cache-busting argument + expect(parameters).toEqual(expectedParameters); + }; + + /** + * Intended for use with POST requests using application/x-www-form-urlencoded. + */ + expectPostRequest = function(requests, url, body, requestIndex) { + var request; + if (_.isUndefined(requestIndex)) { + requestIndex = requests.length - 1; + } + request = requests[requestIndex]; + expect(request.url).toEqual(url); + expect(request.method).toEqual("POST"); + expect(_.difference(request.requestBody.split('&'), body.split('&'))).toEqual([]); + }; + + respondWithJson = function(requests, jsonResponse, requestIndex) { + if (_.isUndefined(requestIndex)) { + requestIndex = requests.length - 1; + } + requests[requestIndex].respond(200, + { 'Content-Type': 'application/json' }, + JSON.stringify(jsonResponse)); + }; + + respondWithError = function(requests, statusCode, jsonResponse, requestIndex) { + if (_.isUndefined(requestIndex)) { + requestIndex = requests.length - 1; + } + if (_.isUndefined(statusCode)) { + statusCode = 500; + } + if (_.isUndefined(jsonResponse)) { + jsonResponse = {}; + } + requests[requestIndex].respond(statusCode, + { 'Content-Type': 'application/json' }, + JSON.stringify(jsonResponse) + ); + }; + + respondWithTextError = function(requests, statusCode, textResponse, requestIndex) { + if (_.isUndefined(requestIndex)) { + requestIndex = requests.length - 1; + } + if (_.isUndefined(statusCode)) { + statusCode = 500; + } + if (_.isUndefined(textResponse)) { + textResponse = ""; + } + requests[requestIndex].respond(statusCode, + { 'Content-Type': 'text/plain' }, + textResponse + ); + }; + + respondWithNoContent = function(requests, requestIndex) { + if (_.isUndefined(requestIndex)) { + requestIndex = requests.length - 1; + } + requests[requestIndex].respond(204, + { 'Content-Type': 'application/json' }); + }; + + return { + 'server': fakeServer, + 'requests': fakeRequests, + 'expectRequest': expectRequest, + 'expectJsonRequest': expectJsonRequest, + 'expectJsonRequestURL': expectJsonRequestURL, + 'expectPostRequest': expectPostRequest, + 'respondWithJson': respondWithJson, + 'respondWithError': respondWithError, + 'respondWithTextError': respondWithTextError, + 'respondWithNoContent': respondWithNoContent, + }; +}); diff --git a/edx_sga/static/js_test.yml b/edx_sga/static/js_test.yml new file mode 100644 index 00000000..296a3e5e --- /dev/null +++ b/edx_sga/static/js_test.yml @@ -0,0 +1,98 @@ +--- +# JavaScript test suite description +# +# +# To run all the tests and print results to the console: +# +# js-test-tool run TEST_SUITE --use-firefox +# +# where `TEST_SUITE` is this file. +# +# +# To run the tests in your default browser ("dev mode"): +# +# js-test-tool dev TEST_SUITE +# + +# Name of the test suite, used to construct +# the URL from which pages are served. +# +# For example, if the suite name is "test_suite", +# then: +# +# * /suite/test_suite +# serves the test suite runner page +# * /suite/test_suite/include/* +# serves dependencies (src, spec, lib, and fixtures) +# +# Test suite names must be URL-encodable and unique +# among suite descriptions passed to js-test-tool +test_suite_name: edx_sga + +# Currently, the only supported test runner is Jasmine +# See http://pivotal.github.io/jasmine/ +# for the Jasmine documentation. +test_runner: jasmine_requirejs + +# Path prepended to source files in the coverage report (optional) +# For example, if the source path +# is "src/source.js" (relative to this YAML file) +# and the prepend path is "base/dir" +# then the coverage report will show +# "base/dir/src/source.js" +prepend_path: static + +# Paths to library JavaScript files (optional) +lib_paths: + - js/bower + +# Paths to source JavaScript files +src_paths: + - js/src + +# Paths to spec (test) JavaScript files +spec_paths: + - js/spec + - js/spec_helpers + +# Paths to fixture files (optional) +# The fixture path will be set automatically when using jasmine-jquery. +# (https://github.com/velesin/jasmine-jquery) +# +# You can then access fixtures using paths relative to +# the test suite description: +# +# loadFixtures('path/to/fixture/fixture.html'); +# +#fixture_paths: +# - js/fixture + +# Regular expressions used to exclude *.js files from +# appearing in the test runner page. +# Some test runners (like the jasmine runner) include files by default, +# which means that they are loaded using a