Commit 461b8059 by Christina Roberts

Merge pull request #1951 from edx/christina/fix-xhr

Workaround for "xhr.restore" failures.
parents 3f947e80 0ef923e0
......@@ -42,9 +42,10 @@ requirejs.config({
"mathjax": "//edx-static.s3.amazonaws.com/mathjax-MathJax-727332c/MathJax.js?config=TeX-MML-AM_HTMLorMML-full&delayStartupUntil=configured",
"youtube": "//www.youtube.com/player_api?noext",
"tender": "//edxedge.tenderapp.com/tender_widget"
"tender": "//edxedge.tenderapp.com/tender_widget",
"coffee/src/ajax_prefix": "xmodule_js/common_static/coffee/src/ajax_prefix"
"coffee/src/ajax_prefix": "xmodule_js/common_static/coffee/src/ajax_prefix",
"js/spec/test_utils": "js/spec/test_utils",
}
shim: {
"gettext": {
......
require ["jquery", "backbone", "coffee/src/main", "sinon", "jasmine-stealth", "jquery.cookie"],
($, Backbone, main, sinon) ->
require ["jquery", "backbone", "coffee/src/main", "js/spec/create_sinon", "jasmine-stealth", "jquery.cookie"],
($, Backbone, main, create_sinon) ->
describe "CMS", ->
it "should initialize URL", ->
expect(window.CMS.URL).toBeDefined()
......@@ -26,30 +26,30 @@ require ["jquery", "backbone", "coffee/src/main", "sinon", "jasmine-stealth", "j
beforeEach ->
setFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(tpl))
appendSetFixtures(sandbox({id: "page-notification"}))
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
afterEach ->
@xhr.restore()
it "successful AJAX request does not pop an error notification", ->
server = create_sinon['server'](200, this)
expect($("#page-notification")).toBeEmpty()
$.ajax("/test")
expect($("#page-notification")).toBeEmpty()
@requests[0].respond(200)
server.respond()
expect($("#page-notification")).toBeEmpty()
it "AJAX request with error should pop an error notification", ->
server = create_sinon['server'](500, this)
$.ajax("/test")
@requests[0].respond(500)
server.respond()
expect($("#page-notification")).not.toBeEmpty()
expect($("#page-notification")).toContain('div.wrapper-notification-error')
it "can override AJAX request with error so it does not pop an error notification", ->
server = create_sinon['server'](500, this)
$.ajax
url: "/test"
notifyOnError: false
@requests[0].respond(500)
expect($("#page-notification")).toBeEmpty()
server.respond()
expect($("#page-notification")).toBeEmpty()
define ["js/models/section", "sinon", "js/utils/module"], (Section, sinon, ModuleUtils) ->
define ["js/models/section", "js/spec/create_sinon", "js/utils/module"], (Section, create_sinon, ModuleUtils) ->
describe "Section", ->
describe "basic", ->
beforeEach ->
......@@ -32,21 +32,19 @@ define ["js/models/section", "sinon", "js/utils/module"], (Section, sinon, Modul
id: 42
name: "Life, the Universe, and Everything"
})
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
afterEach ->
@xhr.restore()
it "show/hide a notification when it saves to the server", ->
server = create_sinon['server'](200, this)
@model.save()
expect(Section.prototype.showNotification).toHaveBeenCalled()
@requests[0].respond(200)
server.respond()
expect(Section.prototype.hideNotification).toHaveBeenCalled()
it "don't hide notification when saving fails", ->
# this is handled by the global AJAX error handler
server = create_sinon['server'](500, this)
@model.save()
@requests[0].respond(500)
server.respond()
expect(Section.prototype.hideNotification).not.toHaveBeenCalled()
define ["jasmine", "sinon", "squire"],
(jasmine, sinon, Squire) ->
define ["jasmine", "js/spec/create_sinon", "squire"],
(jasmine, create_sinon, Squire) ->
feedbackTpl = readFixtures('system-feedback.underscore')
assetTpl = readFixtures('asset.underscore')
......@@ -68,26 +68,20 @@ define ["jasmine", "sinon", "squire"],
expect(@collection).toContain(@model)
describe "AJAX", ->
beforeEach ->
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
afterEach ->
@xhr.restore()
it "should destroy itself on confirmation", ->
requests = create_sinon["requests"](this)
@view.render().$(".remove-asset-button").click()
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
# run the primary function to indicate confirmation
ctorOptions.actions.primary.click(@promptSpies)
# AJAX request has been sent, but not yet returned
expect(@model.destroy).toHaveBeenCalled()
expect(@requests.length).toEqual(1)
expect(requests.length).toEqual(1)
expect(@confirmationSpies.constructor).not.toHaveBeenCalled()
expect(@collection.contains(@model)).toBeTruthy()
# return a success response
@requests[0].respond(200)
requests[0].respond(200)
expect(@confirmationSpies.constructor).toHaveBeenCalled()
expect(@confirmationSpies.show).toHaveBeenCalled()
savingOptions = @confirmationSpies.constructor.mostRecentCall.args[0]
......@@ -95,6 +89,8 @@ define ["jasmine", "sinon", "squire"],
expect(@collection.contains(@model)).toBeFalsy()
it "should not destroy itself if server errors", ->
requests = create_sinon["requests"](this)
@view.render().$(".remove-asset-button").click()
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
# run the primary function to indicate confirmation
......@@ -102,29 +98,33 @@ define ["jasmine", "sinon", "squire"],
# AJAX request has been sent, but not yet returned
expect(@model.destroy).toHaveBeenCalled()
# return an error response
@requests[0].respond(404)
requests[0].respond(404)
expect(@confirmationSpies.constructor).not.toHaveBeenCalled()
expect(@collection.contains(@model)).toBeTruthy()
it "should lock the asset on confirmation", ->
requests = create_sinon["requests"](this)
@view.render().$(".lock-checkbox").click()
# AJAX request has been sent, but not yet returned
expect(@model.save).toHaveBeenCalled()
expect(@requests.length).toEqual(1)
expect(requests.length).toEqual(1)
expect(@savingSpies.constructor).toHaveBeenCalled()
expect(@savingSpies.show).toHaveBeenCalled()
savingOptions = @savingSpies.constructor.mostRecentCall.args[0]
expect(savingOptions.title).toMatch("Saving...")
expect(@model.get("locked")).toBeFalsy()
# return a success response
@requests[0].respond(200)
requests[0].respond(200)
expect(@savingSpies.hide).toHaveBeenCalled()
expect(@model.get("locked")).toBeTruthy()
it "should not lock the asset if server errors", ->
requests = create_sinon["requests"](this)
@view.render().$(".lock-checkbox").click()
# return an error response
@requests[0].respond(404)
requests[0].respond(404)
# Don't call hide because that closes the notification showing the server error.
expect(@savingSpies.hide).not.toHaveBeenCalled()
expect(@model.get("locked")).toBeFalsy()
......@@ -172,9 +172,6 @@ define ["jasmine", "sinon", "squire"],
waitsFor (=> @view), "AssetView was not created", 1000
$.ajax()
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
afterEach ->
delete window.analytics
......@@ -190,18 +187,22 @@ define ["jasmine", "sinon", "squire"],
expect(@view.$el).toContainText("test asset 2")
it "should remove the deleted asset from the view", ->
requests = create_sinon["requests"](this)
# Delete the 2nd asset with success from server.
@view.render().$(".remove-asset-button")[1].click()
@promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
req.respond(200) for req in @requests
req.respond(200) for req in requests
expect(@view.$el).toContainText("test asset 1")
expect(@view.$el).not.toContainText("test asset 2")
it "does not remove asset if deletion failed", ->
requests = create_sinon["requests"](this)
# Delete the 2nd asset, but mimic a failure from the server.
@view.render().$(".remove-asset-button")[1].click()
@promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(@promptSpies)
req.respond(404) for req in @requests
req.respond(404) for req in requests
expect(@view.$el).toContainText("test asset 1")
expect(@view.$el).toContainText("test asset 2")
......
define ["js/views/course_info_handout", "js/views/course_info_update", "js/models/module_info", "js/collections/course_update", "sinon"],
(CourseInfoHandoutsView, CourseInfoUpdateView, ModuleInfo, CourseUpdateCollection, sinon) ->
define ["js/views/course_info_handout", "js/views/course_info_update", "js/models/module_info", "js/collections/course_update", "js/spec/create_sinon"],
(CourseInfoHandoutsView, CourseInfoUpdateView, ModuleInfo, CourseUpdateCollection, create_sinon) ->
describe "Course Updates and Handouts", ->
courseInfoPage = """
<div class="course-info-wrapper">
<div class="main-column window">
......@@ -21,18 +22,13 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
delete window.analytics
delete window.course_location_analytics
xdescribe "Course Updates", ->
describe "Course Updates", ->
courseInfoTemplate = readFixtures('course_info_update.underscore')
beforeEach ->
setFixtures($("<script>", {id: "course_info_update-tpl", type: "text/template"}).text(courseInfoTemplate))
appendSetFixtures courseInfoPage
courseUpdatesXhr = sinon.useFakeXMLHttpRequest()
@courseUpdatesRequests = requests = []
courseUpdatesXhr.onCreate = (xhr) -> requests.push(xhr)
@xhrRestore = courseUpdatesXhr.restore
@collection = new CourseUpdateCollection()
@collection.url = 'course_info_update/'
@courseInfoEdit = new CourseInfoUpdateView({
......@@ -91,10 +87,9 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
else
modalCover.click()
afterEach ->
@xhrRestore()
it "does not rewrite links on save", ->
requests = create_sinon["requests"](this)
# Create a new update, verifying that the model is created
# in the collection and save is called.
expect(@collection.isEmpty()).toBeTruthy()
......@@ -109,7 +104,7 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
expect(model.save).toHaveBeenCalled()
# Verify content sent to server does not have rewritten links.
contentSaved = JSON.parse(@courseUpdatesRequests[@courseUpdatesRequests.length - 1].requestBody).content
contentSaved = JSON.parse(requests[requests.length - 1].requestBody).content
expect(contentSaved).toEqual('/static/image.jpg')
it "does rewrite links for preview", ->
......@@ -139,18 +134,13 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
it "does not remove existing course info on click outside modal", ->
@cancelExistingCourseInfo(false)
xdescribe "Course Handouts", ->
describe "Course Handouts", ->
handoutsTemplate = readFixtures('course_info_handouts.underscore')
beforeEach ->
setFixtures($("<script>", {id: "course_info_handouts-tpl", type: "text/template"}).text(handoutsTemplate))
appendSetFixtures courseInfoPage
courseHandoutsXhr = sinon.useFakeXMLHttpRequest()
@handoutsRequests = requests = []
courseHandoutsXhr.onCreate = (xhr) -> requests.push(xhr)
@handoutsXhrRestore = courseHandoutsXhr.restore
@model = new ModuleInfo({
id: 'handouts-id',
data: '/static/fromServer.jpg'
......@@ -164,10 +154,9 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
@handoutsEdit.render()
afterEach ->
@handoutsXhrRestore()
it "does not rewrite links on save", ->
requests = create_sinon["requests"](this)
# Enter something in the handouts section, verifying that the model is saved
# when "Save" is clicked.
@handoutsEdit.$el.find('.edit-button').click()
......@@ -176,7 +165,7 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
@handoutsEdit.$el.find('.save-button').click()
expect(@model.save).toHaveBeenCalled()
contentSaved = JSON.parse(@handoutsRequests[@handoutsRequests.length - 1].requestBody).data
contentSaved = JSON.parse(requests[requests.length - 1].requestBody).data
expect(contentSaved).toEqual('/static/image.jpg')
it "does rewrite links in initial content", ->
......
define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base", "date", "jquery.timepicker"],
(Overview, Notification, sinon) ->
define ["js/views/overview", "js/views/feedback_notification", "js/spec/create_sinon", "js/base", "date", "jquery.timepicker"],
(Overview, Notification, create_sinon) ->
describe "Course Overview", ->
beforeEach ->
......@@ -95,9 +95,6 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
@notificationSpy = spyOn(Notification.Mini.prototype, 'show').andCallThrough()
window.analytics = jasmine.createSpyObj('analytics', ['track'])
window.course_location_analytics = jasmine.createSpy()
@xhr = sinon.useFakeXMLHttpRequest()
requests = @requests = []
@xhr.onCreate = (req) -> requests.push(req)
Overview.overviewDragger.makeDraggable(
'.unit',
......@@ -135,9 +132,11 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
# expect(@requests[0].url).toEqual('/delete_item')
it "should not delete model when cancel is clicked", ->
requests = create_sinon["requests"](this)
$('a.delete-section-button').click()
$('a.action-secondary').click()
expect(@requests.length).toEqual(0)
expect(requests.length).toEqual(0)
# Fails sporadically in Jenkins.
# it "should show a confirmation on delete", ->
......@@ -402,22 +401,19 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
)
expect($('#subsection-2')).not.toHaveClass('collapsed')
xdescribe "AJAX", ->
describe "AJAX", ->
beforeEach ->
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
@savingSpies = spyOnConstructor(Notification, "Mini",
["show", "hide"])
@savingSpies.show.andReturn(@savingSpies)
@clock = sinon.useFakeTimers()
afterEach ->
@xhr.restore()
@clock.restore()
it "should send an update on reorder", ->
requests = create_sinon["requests"](this)
Overview.overviewDragger.dragState.dropDestination = $('#unit-4')
Overview.overviewDragger.dragState.attachMethod = "after"
Overview.overviewDragger.dragState.parentList = $('#subsection-2')
......@@ -431,7 +427,7 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
null,
{clientX: $('#unit-1').offset().left}
)
expect(@requests.length).toEqual(2)
expect(requests.length).toEqual(2)
expect(@savingSpies.constructor).toHaveBeenCalled()
expect(@savingSpies.show).toHaveBeenCalled()
expect(@savingSpies.hide).not.toHaveBeenCalled()
......@@ -440,11 +436,11 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
expect($('#unit-1')).toHaveClass('was-dropped')
# We expect 2 requests to be sent-- the first for removing Unit 1 from Subsection 1,
# and the second for adding Unit 1 to the end of Subsection 2.
expect(@requests[0].requestBody).toEqual('{"children":["second-unit-id","third-unit-id"]}')
@requests[0].respond(200)
expect(requests[0].requestBody).toEqual('{"children":["second-unit-id","third-unit-id"]}')
requests[0].respond(200)
expect(@savingSpies.hide).not.toHaveBeenCalled()
expect(@requests[1].requestBody).toEqual('{"children":["fourth-unit-id","first-unit-id"]}')
@requests[1].respond(200)
expect(requests[1].requestBody).toEqual('{"children":["fourth-unit-id","first-unit-id"]}')
requests[1].respond(200)
expect(@savingSpies.hide).toHaveBeenCalled()
# Class is removed in a timeout.
@clock.tick(1001)
......
define ["js/models/section", "js/views/section_show", "js/views/section_edit", "sinon"], (Section, SectionShow, SectionEdit, sinon) ->
define ["js/models/section", "js/views/section_show", "js/views/section_edit", "js/spec/create_sinon"], (Section, SectionShow, SectionEdit, create_sinon) ->
describe "SectionShow", ->
describe "Basic", ->
......@@ -39,9 +39,6 @@ define ["js/models/section", "js/views/section_show", "js/views/section_edit", "
.andCallThrough()
window.analytics = jasmine.createSpyObj('analytics', ['track'])
window.course_location_analytics = jasmine.createSpy()
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
@model = new Section({
id: 42
......@@ -51,7 +48,6 @@ define ["js/models/section", "js/views/section_show", "js/views/section_edit", "
@view.render()
afterEach ->
@xhr.restore()
delete window.analytics
delete window.course_location_analytics
......@@ -68,8 +64,10 @@ define ["js/models/section", "js/views/section_show", "js/views/section_edit", "
expect(@model.save).toHaveBeenCalled()
it "should call switchToShowView when save() is successful", ->
requests = create_sinon["requests"](this)
@view.$("input[type=submit]").click()
@requests[0].respond(200)
requests[0].respond(200)
expect(@view.switchToShowView).toHaveBeenCalled()
it "should call showInvalidMessage when validation is unsuccessful", ->
......
define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js/models/course",
"js/collections/textbook", "js/views/show_textbook", "js/views/edit_textbook", "js/views/list_textbooks",
"js/views/edit_chapter", "js/views/feedback_prompt", "js/views/feedback_notification",
"sinon", "jasmine-stealth"],
(Textbook, Chapter, ChapterSet, Course, TextbookSet, ShowTextbook, EditTextbook, ListTexbook, EditChapter, Prompt, Notification, sinon) ->
"js/spec/create_sinon", "jasmine-stealth"],
(Textbook, Chapter, ChapterSet, Course, TextbookSet, ShowTextbook, EditTextbook, ListTexbook, EditChapter, Prompt, Notification, create_sinon) ->
feedbackTpl = readFixtures('system-feedback.underscore')
beforeEach ->
......@@ -74,34 +74,31 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js
describe "AJAX", ->
beforeEach ->
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
@savingSpies = spyOnConstructor(Notification, "Mini",
["show", "hide"])
@savingSpies.show.andReturn(@savingSpies)
CMS.URL.TEXTBOOKS = "/textbooks"
afterEach ->
@xhr.restore()
delete CMS.URL.TEXTBOOKS
it "should destroy itself on confirmation", ->
requests = create_sinon["requests"](this)
@view.render().$(".delete").click()
ctorOptions = @promptSpies.constructor.mostRecentCall.args[0]
# run the primary function to indicate confirmation
ctorOptions.actions.primary.click(@promptSpies)
# AJAX request has been sent, but not yet returned
expect(@model.destroy).toHaveBeenCalled()
expect(@requests.length).toEqual(1)
expect(requests.length).toEqual(1)
expect(@savingSpies.constructor).toHaveBeenCalled()
expect(@savingSpies.show).toHaveBeenCalled()
expect(@savingSpies.hide).not.toHaveBeenCalled()
savingOptions = @savingSpies.constructor.mostRecentCall.args[0]
expect(savingOptions.title).toMatch(/Deleting/)
# return a success response
@requests[0].respond(200)
requests[0].respond(200)
expect(@savingSpies.hide).toHaveBeenCalled()
expect(@collection.contains(@model)).toBeFalsy()
......
define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "sinon"], (FileUpload, UploadDialog, Chapter, sinon) ->
define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "js/spec/create_sinon"], (FileUpload, UploadDialog, Chapter, create_sinon) ->
feedbackTpl = readFixtures('system-feedback.underscore')
......@@ -78,20 +78,18 @@ define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "sinon"],
describe "Uploads", ->
beforeEach ->
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
@xhr.onCreate = (xhr) -> requests.push(xhr)
@clock = sinon.useFakeTimers()
afterEach ->
@xhr.restore()
@clock.restore()
it "can upload correctly", ->
requests = create_sinon["requests"](this)
@view.upload()
expect(@model.get("uploading")).toBeTruthy()
expect(@requests.length).toEqual(1)
request = @requests[0]
expect(requests.length).toEqual(1)
request = requests[0]
expect(request.url).toEqual("/upload")
expect(request.method).toEqual("POST")
......@@ -102,14 +100,18 @@ define ["js/models/uploads", "js/views/uploads", "js/models/chapter", "sinon"],
expect(@dialogResponse.pop()).toEqual("dummy_response")
it "can handle upload errors", ->
requests = create_sinon["requests"](this)
@view.upload()
@requests[0].respond(500)
requests[0].respond(500)
expect(@model.get("title")).toMatch(/error/)
expect(@view.remove).not.toHaveBeenCalled()
it "removes itself after two seconds on successful upload", ->
requests = create_sinon["requests"](this)
@view.upload()
@requests[0].respond(200, {"Content-Type": "application/json"},
requests[0].respond(200, {"Content-Type": "application/json"},
'{"response": "dummy_response"}')
expect(@view.remove).not.toHaveBeenCalled()
@clock.tick(2001)
......
define(["sinon"], function(sinon) {
/* 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-1040.
*/
/**
* Get a reference to the mocked server, and respond
* to all requests with the specified statusCode.
*/
var fakeServer = function (statusCode, that) {
var server = sinon.fakeServer.create();
that.after(function() {
server.restore();
});
server.respondWith([statusCode, {}, '']);
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.
*/
var fakeRequests = function (that) {
var requests = [];
var xhr = sinon.useFakeXMLHttpRequest();
xhr.onCreate = function(request) {
requests.push(request)
};
that.after(function() {
xhr.restore();
});
return requests;
};
return {
"server": fakeServer,
"requests": fakeRequests
};
});
JS_TEST_SUITES = {
'lms' => 'lms/static/js_test.yml',
'cms' => 'cms/static/js_test.yml',
# 'cms-squire' => 'cms/static/js_test_squire.yml',
'cms-squire' => 'cms/static/js_test_squire.yml',
'xmodule' => 'common/lib/xmodule/xmodule/js/js_test.yml',
'common' => 'common/static/js_test.yml',
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment