");
+ 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