Merge pull request #15846 from edx/ormsbee/decaffeinate_tests_2
WIP: Decaffeinate Test Files (Part 2)
This commit is contained in:
1
cms/static/coffee/.gitignore
vendored
1
cms/static/coffee/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
*.js
|
||||
@@ -1,10 +0,0 @@
|
||||
define ["js/models/course"], (Course) ->
|
||||
describe "Course", ->
|
||||
describe "basic", ->
|
||||
beforeEach ->
|
||||
@model = new Course({
|
||||
name: "Greek Hero"
|
||||
})
|
||||
|
||||
it "should take a name argument", ->
|
||||
expect(@model.get("name")).toEqual("Greek Hero")
|
||||
15
cms/static/coffee/spec/models/course_spec.js
Normal file
15
cms/static/coffee/spec/models/course_spec.js
Normal file
@@ -0,0 +1,15 @@
|
||||
define(["js/models/course"], Course =>
|
||||
describe("Course", () =>
|
||||
describe("basic", function() {
|
||||
beforeEach(function() {
|
||||
this.model = new Course({
|
||||
name: "Greek Hero"
|
||||
});
|
||||
});
|
||||
|
||||
it("should take a name argument", function() {
|
||||
expect(this.model.get("name")).toEqual("Greek Hero");
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
@@ -1,59 +0,0 @@
|
||||
define ["js/models/metadata"], (Metadata) ->
|
||||
describe "Metadata", ->
|
||||
it "knows when the value has not been modified", ->
|
||||
model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': false})
|
||||
expect(model.isModified()).toBeFalsy()
|
||||
|
||||
model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': true})
|
||||
model.setValue('original')
|
||||
expect(model.isModified()).toBeFalsy()
|
||||
|
||||
it "knows when the value has been modified", ->
|
||||
model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': false})
|
||||
model.setValue('original')
|
||||
expect(model.isModified()).toBeTruthy()
|
||||
|
||||
model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': true})
|
||||
model.setValue('modified')
|
||||
expect(model.isModified()).toBeTruthy()
|
||||
|
||||
it "tracks when values have been explicitly set", ->
|
||||
model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': false})
|
||||
expect(model.isExplicitlySet()).toBeFalsy()
|
||||
model.setValue('original')
|
||||
expect(model.isExplicitlySet()).toBeTruthy()
|
||||
|
||||
it "has both 'display value' and a 'value' methods", ->
|
||||
model = new Metadata(
|
||||
{'value': 'default', 'explicitly_set': false})
|
||||
expect(model.getValue()).toBeNull
|
||||
expect(model.getDisplayValue()).toBe('default')
|
||||
model.setValue('modified')
|
||||
expect(model.getValue()).toBe('modified')
|
||||
expect(model.getDisplayValue()).toBe('modified')
|
||||
|
||||
it "has a clear method for reverting to the default", ->
|
||||
model = new Metadata(
|
||||
{'value': 'original', 'default_value' : 'default', 'explicitly_set': true})
|
||||
model.clear()
|
||||
expect(model.getValue()).toBeNull
|
||||
expect(model.getDisplayValue()).toBe('default')
|
||||
expect(model.isExplicitlySet()).toBeFalsy()
|
||||
|
||||
it "has a getter for field name", ->
|
||||
model = new Metadata({'field_name': 'foo'})
|
||||
expect(model.getFieldName()).toBe('foo')
|
||||
|
||||
it "has a getter for options", ->
|
||||
model = new Metadata({'options': ['foo', 'bar']})
|
||||
expect(model.getOptions()).toEqual(['foo', 'bar'])
|
||||
|
||||
it "has a getter for type", ->
|
||||
model = new Metadata({'type': 'Integer'})
|
||||
expect(model.getType()).toBe(Metadata.INTEGER_TYPE)
|
||||
|
||||
69
cms/static/coffee/spec/models/metadata_spec.js
Normal file
69
cms/static/coffee/spec/models/metadata_spec.js
Normal file
@@ -0,0 +1,69 @@
|
||||
define(["js/models/metadata"], Metadata =>
|
||||
describe("Metadata", function() {
|
||||
it("knows when the value has not been modified", function() {
|
||||
let model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': false});
|
||||
expect(model.isModified()).toBeFalsy();
|
||||
|
||||
model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': true});
|
||||
model.setValue('original');
|
||||
expect(model.isModified()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("knows when the value has been modified", function() {
|
||||
let model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': false});
|
||||
model.setValue('original');
|
||||
expect(model.isModified()).toBeTruthy();
|
||||
|
||||
model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': true});
|
||||
model.setValue('modified');
|
||||
expect(model.isModified()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("tracks when values have been explicitly set", function() {
|
||||
const model = new Metadata(
|
||||
{'value': 'original', 'explicitly_set': false});
|
||||
expect(model.isExplicitlySet()).toBeFalsy();
|
||||
model.setValue('original');
|
||||
expect(model.isExplicitlySet()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("has both 'display value' and a 'value' methods", function() {
|
||||
const model = new Metadata(
|
||||
{'value': 'default', 'explicitly_set': false});
|
||||
expect(model.getValue()).toBeNull;
|
||||
expect(model.getDisplayValue()).toBe('default');
|
||||
model.setValue('modified');
|
||||
expect(model.getValue()).toBe('modified');
|
||||
expect(model.getDisplayValue()).toBe('modified');
|
||||
});
|
||||
|
||||
it("has a clear method for reverting to the default", function() {
|
||||
const model = new Metadata(
|
||||
{'value': 'original', 'default_value' : 'default', 'explicitly_set': true});
|
||||
model.clear();
|
||||
expect(model.getValue()).toBeNull;
|
||||
expect(model.getDisplayValue()).toBe('default');
|
||||
expect(model.isExplicitlySet()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("has a getter for field name", function() {
|
||||
const model = new Metadata({'field_name': 'foo'});
|
||||
expect(model.getFieldName()).toBe('foo');
|
||||
});
|
||||
|
||||
it("has a getter for options", function() {
|
||||
const model = new Metadata({'options': ['foo', 'bar']});
|
||||
expect(model.getOptions()).toEqual(['foo', 'bar']);
|
||||
});
|
||||
|
||||
it("has a getter for type", function() {
|
||||
const model = new Metadata({'type': 'Integer'});
|
||||
expect(model.getType()).toBe(Metadata.INTEGER_TYPE);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
define ["js/models/section", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "js/utils/module"], (Section, AjaxHelpers, ModuleUtils) ->
|
||||
describe "Section", ->
|
||||
describe "basic", ->
|
||||
beforeEach ->
|
||||
@model = new Section({
|
||||
id: 42
|
||||
name: "Life, the Universe, and Everything"
|
||||
})
|
||||
|
||||
it "should take an id argument", ->
|
||||
expect(@model.get("id")).toEqual(42)
|
||||
|
||||
it "should take a name argument", ->
|
||||
expect(@model.get("name")).toEqual("Life, the Universe, and Everything")
|
||||
|
||||
it "should have a URL set", ->
|
||||
expect(@model.url()).toEqual(ModuleUtils.getUpdateUrl(42))
|
||||
|
||||
it "should serialize to JSON correctly", ->
|
||||
expect(@model.toJSON()).toEqual({
|
||||
metadata:
|
||||
{
|
||||
display_name: "Life, the Universe, and Everything"
|
||||
}
|
||||
})
|
||||
|
||||
describe "XHR", ->
|
||||
beforeEach ->
|
||||
spyOn(Section.prototype, 'showNotification')
|
||||
spyOn(Section.prototype, 'hideNotification')
|
||||
@model = new Section({
|
||||
id: 42
|
||||
name: "Life, the Universe, and Everything"
|
||||
})
|
||||
|
||||
it "show/hide a notification when it saves to the server", ->
|
||||
server = AjaxHelpers.server([200, {"Content-Type": "application/json"}, "{}"])
|
||||
|
||||
@model.save()
|
||||
expect(Section.prototype.showNotification).toHaveBeenCalled()
|
||||
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 = AjaxHelpers.server([500, {"Content-Type": "application/json"}, "{}"])
|
||||
|
||||
@model.save()
|
||||
server.respond()
|
||||
expect(Section.prototype.hideNotification).not.toHaveBeenCalled()
|
||||
67
cms/static/coffee/spec/models/section_spec.js
Normal file
67
cms/static/coffee/spec/models/section_spec.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
define(["js/models/section", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "js/utils/module"], (Section, AjaxHelpers, ModuleUtils) =>
|
||||
describe("Section", function() {
|
||||
describe("basic", function() {
|
||||
beforeEach(function() {
|
||||
this.model = new Section({
|
||||
id: 42,
|
||||
name: "Life, the Universe, and Everything"
|
||||
});
|
||||
});
|
||||
|
||||
it("should take an id argument", function() {
|
||||
expect(this.model.get("id")).toEqual(42);
|
||||
});
|
||||
|
||||
it("should take a name argument", function() {
|
||||
expect(this.model.get("name")).toEqual("Life, the Universe, and Everything");
|
||||
});
|
||||
|
||||
it("should have a URL set", function() {
|
||||
expect(this.model.url()).toEqual(ModuleUtils.getUpdateUrl(42));
|
||||
});
|
||||
|
||||
it("should serialize to JSON correctly", function() {
|
||||
expect(this.model.toJSON()).toEqual({
|
||||
metadata:
|
||||
{
|
||||
display_name: "Life, the Universe, and Everything"
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("XHR", function() {
|
||||
beforeEach(function() {
|
||||
spyOn(Section.prototype, 'showNotification');
|
||||
spyOn(Section.prototype, 'hideNotification');
|
||||
this.model = new Section({
|
||||
id: 42,
|
||||
name: "Life, the Universe, and Everything"
|
||||
});
|
||||
});
|
||||
|
||||
it("show/hide a notification when it saves to the server", function() {
|
||||
const server = AjaxHelpers.server([200, {"Content-Type": "application/json"}, "{}"]);
|
||||
|
||||
this.model.save();
|
||||
expect(Section.prototype.showNotification).toHaveBeenCalled();
|
||||
server.respond();
|
||||
expect(Section.prototype.hideNotification).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("don't hide notification when saving fails", function() {
|
||||
// this is handled by the global AJAX error handler
|
||||
const server = AjaxHelpers.server([500, {"Content-Type": "application/json"}, "{}"]);
|
||||
|
||||
this.model.save();
|
||||
server.respond();
|
||||
expect(Section.prototype.hideNotification).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
@@ -1,39 +0,0 @@
|
||||
define ["js/models/settings/course_grader"], (CourseGrader) ->
|
||||
describe "CourseGraderModel", ->
|
||||
describe "parseWeight", ->
|
||||
it "converts a float to an integer", ->
|
||||
model = new CourseGrader({weight: 7.0001, min_count: 3.67, drop_count: 1.88}, {parse:true})
|
||||
expect(model.get('weight')).toBe(7)
|
||||
expect(model.get('min_count')).toBe(4)
|
||||
expect(model.get('drop_count')).toBe(2)
|
||||
|
||||
it "converts float value of weight to an integer with rounding", ->
|
||||
model = new CourseGrader({weight: 28.999999999999996}, {parse:true})
|
||||
expect(model.get('weight')).toBe(29)
|
||||
|
||||
it "converts a string to an integer", ->
|
||||
model = new CourseGrader({weight: '7.0001', min_count: '3.67', drop_count: '1.88'}, {parse:true})
|
||||
expect(model.get('weight')).toBe(7)
|
||||
expect(model.get('min_count')).toBe(4)
|
||||
expect(model.get('drop_count')).toBe(2)
|
||||
|
||||
it "does a no-op for integers", ->
|
||||
model = new CourseGrader({weight: 7, min_count: 3, drop_count: 1}, {parse:true})
|
||||
expect(model.get('weight')).toBe(7)
|
||||
expect(model.get('min_count')).toBe(3)
|
||||
expect(model.get('drop_count')).toBe(1)
|
||||
|
||||
it "gives validation error if min_count is less than 1 or drop_count is NaN", ->
|
||||
model = new CourseGrader()
|
||||
errors = model.validate({min_count: 0, drop_count: ''}, {validate:true})
|
||||
expect(errors.min_count).toBe('Please enter an integer greater than 0.')
|
||||
expect(errors.drop_count).toBe('Please enter non-negative integer.')
|
||||
# don't allow negative integers
|
||||
errors = model.validate({min_count: -12, drop_count: -1}, {validate:true})
|
||||
expect(errors.min_count).toBe('Please enter an integer greater than 0.')
|
||||
expect(errors.drop_count).toBe('Please enter non-negative integer.')
|
||||
# don't allow floats
|
||||
errors = model.validate({min_count: 12.2, drop_count: 1.5}, {validate:true})
|
||||
expect(errors.min_count).toBe('Please enter an integer greater than 0.')
|
||||
expect(errors.drop_count).toBe('Please enter non-negative integer.')
|
||||
|
||||
47
cms/static/coffee/spec/models/settings_course_grader_spec.js
Normal file
47
cms/static/coffee/spec/models/settings_course_grader_spec.js
Normal file
@@ -0,0 +1,47 @@
|
||||
define(["js/models/settings/course_grader"], CourseGrader =>
|
||||
describe("CourseGraderModel", () =>
|
||||
describe("parseWeight", function() {
|
||||
it("converts a float to an integer", function() {
|
||||
const model = new CourseGrader({weight: 7.0001, min_count: 3.67, drop_count: 1.88}, {parse:true});
|
||||
expect(model.get('weight')).toBe(7);
|
||||
expect(model.get('min_count')).toBe(4);
|
||||
expect(model.get('drop_count')).toBe(2);
|
||||
});
|
||||
|
||||
it("converts float value of weight to an integer with rounding", function() {
|
||||
const model = new CourseGrader({weight: 28.999999999999996}, {parse:true});
|
||||
expect(model.get('weight')).toBe(29);
|
||||
});
|
||||
|
||||
it("converts a string to an integer", function() {
|
||||
const model = new CourseGrader({weight: '7.0001', min_count: '3.67', drop_count: '1.88'}, {parse:true});
|
||||
expect(model.get('weight')).toBe(7);
|
||||
expect(model.get('min_count')).toBe(4);
|
||||
expect(model.get('drop_count')).toBe(2);
|
||||
});
|
||||
|
||||
it("does a no-op for integers", function() {
|
||||
const model = new CourseGrader({weight: 7, min_count: 3, drop_count: 1}, {parse:true});
|
||||
expect(model.get('weight')).toBe(7);
|
||||
expect(model.get('min_count')).toBe(3);
|
||||
expect(model.get('drop_count')).toBe(1);
|
||||
});
|
||||
|
||||
it("gives validation error if min_count is less than 1 or drop_count is NaN", function() {
|
||||
const model = new CourseGrader();
|
||||
let errors = model.validate({min_count: 0, drop_count: ''}, {validate:true});
|
||||
expect(errors.min_count).toBe('Please enter an integer greater than 0.');
|
||||
expect(errors.drop_count).toBe('Please enter non-negative integer.');
|
||||
// don't allow negative integers
|
||||
errors = model.validate({min_count: -12, drop_count: -1}, {validate:true});
|
||||
expect(errors.min_count).toBe('Please enter an integer greater than 0.');
|
||||
expect(errors.drop_count).toBe('Please enter non-negative integer.');
|
||||
// don't allow floats
|
||||
errors = model.validate({min_count: 12.2, drop_count: 1.5}, {validate:true});
|
||||
expect(errors.min_count).toBe('Please enter an integer greater than 0.');
|
||||
expect(errors.drop_count).toBe('Please enter non-negative integer.');
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
define ["underscore", "js/models/settings/course_grading_policy"], (_, CourseGradingPolicy) ->
|
||||
describe "CourseGradingPolicy", ->
|
||||
beforeEach ->
|
||||
@model = new CourseGradingPolicy()
|
||||
|
||||
describe "parse", ->
|
||||
it "sets a null grace period to 00:00", ->
|
||||
attrs = @model.parse(grace_period: null)
|
||||
expect(attrs.grace_period).toEqual(
|
||||
hours: 0,
|
||||
minutes: 0
|
||||
)
|
||||
|
||||
describe "parseGracePeriod", ->
|
||||
it "parses a time in HH:MM format", ->
|
||||
time = @model.parseGracePeriod("07:19")
|
||||
expect(time).toEqual(
|
||||
hours: 7,
|
||||
minutes: 19
|
||||
)
|
||||
|
||||
it "returns null on an incorrectly formatted string", ->
|
||||
expect(@model.parseGracePeriod("asdf")).toBe(null)
|
||||
expect(@model.parseGracePeriod("7:19")).toBe(null)
|
||||
expect(@model.parseGracePeriod("1000:00")).toBe(null)
|
||||
|
||||
describe "validate", ->
|
||||
it "enforces that the passing grade is <= the minimum grade to receive credit if credit is enabled", ->
|
||||
@model.set({minimum_grade_credit: 0.8, grace_period: '01:00', is_credit_course: true})
|
||||
@model.set('grade_cutoffs', [0.9], validate: true)
|
||||
expect(_.keys(@model.validationError)).toContain('minimum_grade_credit')
|
||||
|
||||
it "does not enforce the passing grade limit in non-credit courses", ->
|
||||
@model.set({minimum_grade_credit: 0.8, grace_period: '01:00', is_credit_course: false})
|
||||
@model.set({grade_cutoffs: [0.9]}, validate: true)
|
||||
expect(@model.validationError).toBe(null)
|
||||
47
cms/static/coffee/spec/models/settings_grading_spec.js
Normal file
47
cms/static/coffee/spec/models/settings_grading_spec.js
Normal file
@@ -0,0 +1,47 @@
|
||||
define(["underscore", "js/models/settings/course_grading_policy"], (_, CourseGradingPolicy) =>
|
||||
describe("CourseGradingPolicy", function() {
|
||||
beforeEach(function() {
|
||||
return this.model = new CourseGradingPolicy();
|
||||
});
|
||||
|
||||
describe("parse", () =>
|
||||
it("sets a null grace period to 00:00", function() {
|
||||
const attrs = this.model.parse({grace_period: null});
|
||||
expect(attrs.grace_period).toEqual({
|
||||
hours: 0,
|
||||
minutes: 0
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
describe("parseGracePeriod", function() {
|
||||
it("parses a time in HH:MM format", function() {
|
||||
const time = this.model.parseGracePeriod("07:19");
|
||||
expect(time).toEqual({
|
||||
hours: 7,
|
||||
minutes: 19
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null on an incorrectly formatted string", function() {
|
||||
expect(this.model.parseGracePeriod("asdf")).toBe(null);
|
||||
expect(this.model.parseGracePeriod("7:19")).toBe(null);
|
||||
expect(this.model.parseGracePeriod("1000:00")).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validate", function() {
|
||||
it("enforces that the passing grade is <= the minimum grade to receive credit if credit is enabled", function() {
|
||||
this.model.set({minimum_grade_credit: 0.8, grace_period: '01:00', is_credit_course: true});
|
||||
this.model.set('grade_cutoffs', [0.9], {validate: true});
|
||||
expect(_.keys(this.model.validationError)).toContain('minimum_grade_credit');
|
||||
});
|
||||
|
||||
it("does not enforce the passing grade limit in non-credit courses", function() {
|
||||
this.model.set({minimum_grade_credit: 0.8, grace_period: '01:00', is_credit_course: false});
|
||||
this.model.set({grade_cutoffs: [0.9]}, {validate: true});
|
||||
expect(this.model.validationError).toBe(null);
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
@@ -1,198 +0,0 @@
|
||||
|
||||
define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/chapter", "js/collections/chapter", "cms/js/main"],
|
||||
(Backbone, Textbook, TextbookSet, Chapter, ChapterSet, main) ->
|
||||
|
||||
describe "Textbook model", ->
|
||||
beforeEach ->
|
||||
main()
|
||||
@model = new Textbook()
|
||||
CMS.URL.TEXTBOOKS = "/textbooks"
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.TEXTBOOKS
|
||||
|
||||
describe "Basic", ->
|
||||
it "should have an empty name by default", ->
|
||||
expect(@model.get("name")).toEqual("")
|
||||
|
||||
it "should not show chapters by default", ->
|
||||
expect(@model.get("showChapters")).toBeFalsy()
|
||||
|
||||
it "should have a ChapterSet with one chapter by default", ->
|
||||
chapters = @model.get("chapters")
|
||||
expect(chapters).toBeInstanceOf(ChapterSet)
|
||||
expect(chapters.length).toEqual(1)
|
||||
expect(chapters.at(0).isEmpty()).toBeTruthy()
|
||||
|
||||
it "should be empty by default", ->
|
||||
expect(@model.isEmpty()).toBeTruthy()
|
||||
|
||||
it "should have a URL root", ->
|
||||
urlRoot = _.result(@model, 'urlRoot')
|
||||
expect(urlRoot).toBeTruthy()
|
||||
|
||||
it "should be able to reset itself", ->
|
||||
@model.set("name", "foobar")
|
||||
@model.reset()
|
||||
expect(@model.get("name")).toEqual("")
|
||||
|
||||
it "should not be dirty by default", ->
|
||||
expect(@model.isDirty()).toBeFalsy()
|
||||
|
||||
it "should be dirty after it's been changed", ->
|
||||
@model.set("name", "foobar")
|
||||
expect(@model.isDirty()).toBeTruthy()
|
||||
|
||||
it "should not be dirty after calling setOriginalAttributes", ->
|
||||
@model.set("name", "foobar")
|
||||
@model.setOriginalAttributes()
|
||||
expect(@model.isDirty()).toBeFalsy()
|
||||
|
||||
describe "Input/Output", ->
|
||||
deepAttributes = (obj) ->
|
||||
if obj instanceof Backbone.Model
|
||||
deepAttributes(obj.attributes)
|
||||
else if obj instanceof Backbone.Collection
|
||||
obj.map(deepAttributes);
|
||||
else if _.isArray(obj)
|
||||
_.map(obj, deepAttributes);
|
||||
else if _.isObject(obj)
|
||||
attributes = {};
|
||||
for own prop, val of obj
|
||||
attributes[prop] = deepAttributes(val)
|
||||
attributes
|
||||
else
|
||||
obj
|
||||
|
||||
it "should match server model to client model", ->
|
||||
serverModelSpec = {
|
||||
"tab_title": "My Textbook",
|
||||
"chapters": [
|
||||
{"title": "Chapter 1", "url": "/ch1.pdf"},
|
||||
{"title": "Chapter 2", "url": "/ch2.pdf"},
|
||||
]
|
||||
}
|
||||
clientModelSpec = {
|
||||
"name": "My Textbook",
|
||||
"showChapters": false,
|
||||
"editing": false,
|
||||
"chapters": [{
|
||||
"name": "Chapter 1",
|
||||
"asset_path": "/ch1.pdf",
|
||||
"order": 1
|
||||
}, {
|
||||
"name": "Chapter 2",
|
||||
"asset_path": "/ch2.pdf",
|
||||
"order": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
model = new Textbook(serverModelSpec, {parse: true})
|
||||
expect(deepAttributes(model)).toEqual(clientModelSpec)
|
||||
expect(model.toJSON()).toEqual(serverModelSpec)
|
||||
|
||||
describe "Validation", ->
|
||||
it "requires a name", ->
|
||||
model = new Textbook({name: ""})
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "requires at least one chapter", ->
|
||||
model = new Textbook({name: "foo"})
|
||||
model.get("chapters").reset()
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "requires a valid chapter", ->
|
||||
chapter = new Chapter()
|
||||
chapter.isValid = -> false
|
||||
model = new Textbook({name: "foo"})
|
||||
model.get("chapters").reset([chapter])
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "requires all chapters to be valid", ->
|
||||
chapter1 = new Chapter()
|
||||
chapter1.isValid = -> true
|
||||
chapter2 = new Chapter()
|
||||
chapter2.isValid = -> false
|
||||
model = new Textbook({name: "foo"})
|
||||
model.get("chapters").reset([chapter1, chapter2])
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "can pass validation", ->
|
||||
chapter = new Chapter()
|
||||
chapter.isValid = -> true
|
||||
model = new Textbook({name: "foo"})
|
||||
model.get("chapters").reset([chapter])
|
||||
expect(model.isValid()).toBeTruthy()
|
||||
|
||||
|
||||
describe "Textbook collection", ->
|
||||
beforeEach ->
|
||||
CMS.URL.TEXTBOOKS = "/textbooks"
|
||||
@collection = new TextbookSet()
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.TEXTBOOKS
|
||||
|
||||
it "should have a url set", ->
|
||||
url = _.result(@collection, 'url')
|
||||
expect(url).toEqual("/textbooks")
|
||||
|
||||
|
||||
describe "Chapter model", ->
|
||||
beforeEach ->
|
||||
@model = new Chapter()
|
||||
|
||||
describe "Basic", ->
|
||||
it "should have a name by default", ->
|
||||
expect(@model.get("name")).toEqual("")
|
||||
|
||||
it "should have an asset_path by default", ->
|
||||
expect(@model.get("asset_path")).toEqual("")
|
||||
|
||||
it "should have an order by default", ->
|
||||
expect(@model.get("order")).toEqual(1)
|
||||
|
||||
it "should be empty by default", ->
|
||||
expect(@model.isEmpty()).toBeTruthy()
|
||||
|
||||
describe "Validation", ->
|
||||
it "requires a name", ->
|
||||
model = new Chapter({name: "", asset_path: "a.pdf"})
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "requires an asset_path", ->
|
||||
model = new Chapter({name: "a", asset_path: ""})
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
|
||||
it "can pass validation", ->
|
||||
model = new Chapter({name: "a", asset_path: "a.pdf"})
|
||||
expect(model.isValid()).toBeTruthy()
|
||||
|
||||
|
||||
describe "Chapter collection", ->
|
||||
beforeEach ->
|
||||
@collection = new ChapterSet()
|
||||
|
||||
it "is empty by default", ->
|
||||
expect(@collection.isEmpty()).toBeTruthy()
|
||||
|
||||
it "is empty if all chapters are empty", ->
|
||||
@collection.add([{}, {}, {}])
|
||||
expect(@collection.isEmpty()).toBeTruthy()
|
||||
|
||||
it "is not empty if a chapter is not empty", ->
|
||||
@collection.add([{}, {name: "full"}, {}])
|
||||
expect(@collection.isEmpty()).toBeFalsy()
|
||||
|
||||
it "should have a nextOrder function", ->
|
||||
expect(@collection.nextOrder()).toEqual(1)
|
||||
@collection.add([{}])
|
||||
expect(@collection.nextOrder()).toEqual(2)
|
||||
@collection.add([{}])
|
||||
expect(@collection.nextOrder()).toEqual(3)
|
||||
# verify that it doesn't just return an incrementing value each time
|
||||
expect(@collection.nextOrder()).toEqual(3)
|
||||
# try going back one
|
||||
@collection.remove(@collection.last())
|
||||
expect(@collection.nextOrder()).toEqual(2)
|
||||
240
cms/static/coffee/spec/models/textbook_spec.js
Normal file
240
cms/static/coffee/spec/models/textbook_spec.js
Normal file
@@ -0,0 +1,240 @@
|
||||
define(["backbone", "js/models/textbook", "js/collections/textbook", "js/models/chapter", "js/collections/chapter", "cms/js/main"],
|
||||
function(Backbone, Textbook, TextbookSet, Chapter, ChapterSet, main) {
|
||||
|
||||
describe("Textbook model", function() {
|
||||
beforeEach(function() {
|
||||
main();
|
||||
this.model = new Textbook();
|
||||
CMS.URL.TEXTBOOKS = "/textbooks";
|
||||
});
|
||||
|
||||
afterEach(() => delete CMS.URL.TEXTBOOKS);
|
||||
|
||||
describe("Basic", function() {
|
||||
it("should have an empty name by default", function() {
|
||||
expect(this.model.get("name")).toEqual("");
|
||||
});
|
||||
|
||||
it("should not show chapters by default", function() {
|
||||
expect(this.model.get("showChapters")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should have a ChapterSet with one chapter by default", function() {
|
||||
const chapters = this.model.get("chapters");
|
||||
expect(chapters).toBeInstanceOf(ChapterSet);
|
||||
expect(chapters.length).toEqual(1);
|
||||
expect(chapters.at(0).isEmpty()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should be empty by default", function() {
|
||||
expect(this.model.isEmpty()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should have a URL root", function() {
|
||||
const urlRoot = _.result(this.model, 'urlRoot');
|
||||
expect(urlRoot).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should be able to reset itself", function() {
|
||||
this.model.set("name", "foobar");
|
||||
this.model.reset();
|
||||
expect(this.model.get("name")).toEqual("");
|
||||
});
|
||||
|
||||
it("should not be dirty by default", function() {
|
||||
expect(this.model.isDirty()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should be dirty after it's been changed", function() {
|
||||
this.model.set("name", "foobar");
|
||||
expect(this.model.isDirty()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not be dirty after calling setOriginalAttributes", function() {
|
||||
this.model.set("name", "foobar");
|
||||
this.model.setOriginalAttributes();
|
||||
expect(this.model.isDirty()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Input/Output", function() {
|
||||
var deepAttributes = function(obj) {
|
||||
if (obj instanceof Backbone.Model) {
|
||||
return deepAttributes(obj.attributes);
|
||||
} else if (obj instanceof Backbone.Collection) {
|
||||
return obj.map(deepAttributes);
|
||||
} else if (_.isArray(obj)) {
|
||||
return _.map(obj, deepAttributes);
|
||||
} else if (_.isObject(obj)) {
|
||||
const attributes = {};
|
||||
for (let prop of Object.keys(obj)) {
|
||||
const val = obj[prop];
|
||||
attributes[prop] = deepAttributes(val);
|
||||
}
|
||||
return attributes;
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
};
|
||||
|
||||
it("should match server model to client model", function() {
|
||||
const serverModelSpec = {
|
||||
"tab_title": "My Textbook",
|
||||
"chapters": [
|
||||
{"title": "Chapter 1", "url": "/ch1.pdf"},
|
||||
{"title": "Chapter 2", "url": "/ch2.pdf"},
|
||||
]
|
||||
};
|
||||
const clientModelSpec = {
|
||||
"name": "My Textbook",
|
||||
"showChapters": false,
|
||||
"editing": false,
|
||||
"chapters": [{
|
||||
"name": "Chapter 1",
|
||||
"asset_path": "/ch1.pdf",
|
||||
"order": 1
|
||||
}, {
|
||||
"name": "Chapter 2",
|
||||
"asset_path": "/ch2.pdf",
|
||||
"order": 2
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const model = new Textbook(serverModelSpec, {parse: true});
|
||||
expect(deepAttributes(model)).toEqual(clientModelSpec);
|
||||
expect(model.toJSON()).toEqual(serverModelSpec);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Validation", function() {
|
||||
it("requires a name", function() {
|
||||
const model = new Textbook({name: ""});
|
||||
expect(model.isValid()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("requires at least one chapter", function() {
|
||||
const model = new Textbook({name: "foo"});
|
||||
model.get("chapters").reset();
|
||||
expect(model.isValid()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("requires a valid chapter", function() {
|
||||
const chapter = new Chapter();
|
||||
chapter.isValid = () => false;
|
||||
const model = new Textbook({name: "foo"});
|
||||
model.get("chapters").reset([chapter]);
|
||||
expect(model.isValid()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("requires all chapters to be valid", function() {
|
||||
const chapter1 = new Chapter();
|
||||
chapter1.isValid = () => true;
|
||||
const chapter2 = new Chapter();
|
||||
chapter2.isValid = () => false;
|
||||
const model = new Textbook({name: "foo"});
|
||||
model.get("chapters").reset([chapter1, chapter2]);
|
||||
expect(model.isValid()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("can pass validation", function() {
|
||||
const chapter = new Chapter();
|
||||
chapter.isValid = () => true;
|
||||
const model = new Textbook({name: "foo"});
|
||||
model.get("chapters").reset([chapter]);
|
||||
expect(model.isValid()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("Textbook collection", function() {
|
||||
beforeEach(function() {
|
||||
CMS.URL.TEXTBOOKS = "/textbooks";
|
||||
this.collection = new TextbookSet();
|
||||
});
|
||||
|
||||
afterEach(() => delete CMS.URL.TEXTBOOKS);
|
||||
|
||||
it("should have a url set", function() {
|
||||
const url = _.result(this.collection, 'url');
|
||||
expect(url).toEqual("/textbooks");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("Chapter model", function() {
|
||||
beforeEach(function() {
|
||||
this.model = new Chapter();
|
||||
});
|
||||
|
||||
describe("Basic", function() {
|
||||
it("should have a name by default", function() {
|
||||
expect(this.model.get("name")).toEqual("");
|
||||
});
|
||||
|
||||
it("should have an asset_path by default", function() {
|
||||
expect(this.model.get("asset_path")).toEqual("");
|
||||
});
|
||||
|
||||
it("should have an order by default", function() {
|
||||
expect(this.model.get("order")).toEqual(1);
|
||||
});
|
||||
|
||||
it("should be empty by default", function() {
|
||||
expect(this.model.isEmpty()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Validation", function() {
|
||||
it("requires a name", function() {
|
||||
const model = new Chapter({name: "", asset_path: "a.pdf"});
|
||||
expect(model.isValid()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("requires an asset_path", function() {
|
||||
const model = new Chapter({name: "a", asset_path: ""});
|
||||
expect(model.isValid()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("can pass validation", function() {
|
||||
const model = new Chapter({name: "a", asset_path: "a.pdf"});
|
||||
expect(model.isValid()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("Chapter collection", function() {
|
||||
beforeEach(function() {
|
||||
this.collection = new ChapterSet();
|
||||
});
|
||||
|
||||
it("is empty by default", function() {
|
||||
expect(this.collection.isEmpty()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("is empty if all chapters are empty", function() {
|
||||
this.collection.add([{}, {}, {}]);
|
||||
expect(this.collection.isEmpty()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("is not empty if a chapter is not empty", function() {
|
||||
this.collection.add([{}, {name: "full"}, {}]);
|
||||
expect(this.collection.isEmpty()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should have a nextOrder function", function() {
|
||||
expect(this.collection.nextOrder()).toEqual(1);
|
||||
this.collection.add([{}]);
|
||||
expect(this.collection.nextOrder()).toEqual(2);
|
||||
this.collection.add([{}]);
|
||||
expect(this.collection.nextOrder()).toEqual(3);
|
||||
// verify that it doesn't just return an incrementing value each time
|
||||
expect(this.collection.nextOrder()).toEqual(3);
|
||||
// try going back one
|
||||
this.collection.remove(this.collection.last());
|
||||
expect(this.collection.nextOrder()).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
define ["js/models/uploads"], (FileUpload) ->
|
||||
|
||||
describe "FileUpload", ->
|
||||
beforeEach ->
|
||||
@model = new FileUpload()
|
||||
|
||||
it "is unfinished by default", ->
|
||||
expect(@model.get("finished")).toBeFalsy()
|
||||
|
||||
it "is not uploading by default", ->
|
||||
expect(@model.get("uploading")).toBeFalsy()
|
||||
|
||||
it "is valid by default", ->
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
it "is valid for text files by default", ->
|
||||
file = {"type": "text/plain", "name": "filename.txt"}
|
||||
@model.set("selectedFile", file);
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
it "is valid for PNG files by default", ->
|
||||
file = {"type": "image/png", "name": "filename.png"}
|
||||
@model.set("selectedFile", file);
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
it "can accept a file type when explicitly set", ->
|
||||
file = {"type": "image/png", "name": "filename.png"}
|
||||
@model.set("mimeTypes": ["image/png"])
|
||||
@model.set("selectedFile", file)
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
it "can accept a file format when explicitly set", ->
|
||||
file = {"type": "", "name": "filename.png"}
|
||||
@model.set("fileFormats": ["png"])
|
||||
@model.set("selectedFile", file)
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
it "can accept multiple file types", ->
|
||||
file = {"type": "image/gif", "name": "filename.gif"}
|
||||
@model.set("mimeTypes": ["image/png", "image/jpeg", "image/gif"])
|
||||
@model.set("selectedFile", file)
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
it "can accept multiple file formats", ->
|
||||
file = {"type": "image/gif", "name": "filename.gif"}
|
||||
@model.set("fileFormats": ["png", "jpeg", "gif"])
|
||||
@model.set("selectedFile", file)
|
||||
expect(@model.isValid()).toBeTruthy()
|
||||
|
||||
describe "fileTypes", ->
|
||||
it "returns a list of the uploader's file types", ->
|
||||
@model.set('mimeTypes', ['image/png', 'application/json'])
|
||||
@model.set('fileFormats', ['gif', 'srt'])
|
||||
expect(@model.fileTypes()).toEqual(['PNG', 'JSON', 'GIF', 'SRT'])
|
||||
|
||||
describe "formatValidTypes", ->
|
||||
it "returns a map of formatted file types and extensions", ->
|
||||
@model.set('mimeTypes', ['image/png', 'image/jpeg', 'application/json'])
|
||||
formatted = @model.formatValidTypes()
|
||||
expect(formatted).toEqual(
|
||||
fileTypes: 'PNG, JPEG or JSON',
|
||||
fileExtensions: '.png, .jpeg or .json'
|
||||
)
|
||||
|
||||
it "does not format with only one mime type", ->
|
||||
@model.set('mimeTypes', ['application/pdf'])
|
||||
formatted = @model.formatValidTypes()
|
||||
expect(formatted).toEqual(
|
||||
fileTypes: 'PDF',
|
||||
fileExtensions: '.pdf'
|
||||
)
|
||||
88
cms/static/coffee/spec/models/upload_spec.js
Normal file
88
cms/static/coffee/spec/models/upload_spec.js
Normal file
@@ -0,0 +1,88 @@
|
||||
define(["js/models/uploads"], FileUpload =>
|
||||
|
||||
describe("FileUpload", function() {
|
||||
beforeEach(function() {
|
||||
this.model = new FileUpload();
|
||||
});
|
||||
|
||||
it("is unfinished by default", function() {
|
||||
expect(this.model.get("finished")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("is not uploading by default", function() {
|
||||
expect(this.model.get("uploading")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("is valid by default", function() {
|
||||
expect(this.model.isValid()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("is valid for text files by default", function() {
|
||||
const file = {"type": "text/plain", "name": "filename.txt"};
|
||||
this.model.set("selectedFile", file);
|
||||
expect(this.model.isValid()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("is valid for PNG files by default", function() {
|
||||
const file = {"type": "image/png", "name": "filename.png"};
|
||||
this.model.set("selectedFile", file);
|
||||
expect(this.model.isValid()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("can accept a file type when explicitly set", function() {
|
||||
const file = {"type": "image/png", "name": "filename.png"};
|
||||
this.model.set({"mimeTypes": ["image/png"]});
|
||||
this.model.set("selectedFile", file);
|
||||
expect(this.model.isValid()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("can accept a file format when explicitly set", function() {
|
||||
const file = {"type": "", "name": "filename.png"};
|
||||
this.model.set({"fileFormats": ["png"]});
|
||||
this.model.set("selectedFile", file);
|
||||
expect(this.model.isValid()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("can accept multiple file types", function() {
|
||||
const file = {"type": "image/gif", "name": "filename.gif"};
|
||||
this.model.set({"mimeTypes": ["image/png", "image/jpeg", "image/gif"]});
|
||||
this.model.set("selectedFile", file);
|
||||
expect(this.model.isValid()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("can accept multiple file formats", function() {
|
||||
const file = {"type": "image/gif", "name": "filename.gif"};
|
||||
this.model.set({"fileFormats": ["png", "jpeg", "gif"]});
|
||||
this.model.set("selectedFile", file);
|
||||
expect(this.model.isValid()).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("fileTypes", () =>
|
||||
it("returns a list of the uploader's file types", function() {
|
||||
this.model.set('mimeTypes', ['image/png', 'application/json']);
|
||||
this.model.set('fileFormats', ['gif', 'srt']);
|
||||
expect(this.model.fileTypes()).toEqual(['PNG', 'JSON', 'GIF', 'SRT']);
|
||||
})
|
||||
);
|
||||
|
||||
describe("formatValidTypes", function() {
|
||||
it("returns a map of formatted file types and extensions", function() {
|
||||
this.model.set('mimeTypes', ['image/png', 'image/jpeg', 'application/json']);
|
||||
const formatted = this.model.formatValidTypes();
|
||||
expect(formatted).toEqual({
|
||||
fileTypes: 'PNG, JPEG or JSON',
|
||||
fileExtensions: '.png, .jpeg or .json'
|
||||
});
|
||||
});
|
||||
|
||||
it("does not format with only one mime type", function() {
|
||||
this.model.set('mimeTypes', ['application/pdf']);
|
||||
const formatted = this.model.formatValidTypes();
|
||||
expect(formatted).toEqual({
|
||||
fileTypes: 'PDF',
|
||||
fileExtensions: '.pdf'
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
@@ -1,369 +0,0 @@
|
||||
define ["jquery", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "squire"],
|
||||
($, AjaxHelpers, Squire) ->
|
||||
|
||||
assetLibraryTpl = readFixtures('asset-library.underscore')
|
||||
assetTpl = readFixtures('asset.underscore')
|
||||
|
||||
describe "Asset view", ->
|
||||
beforeEach (done) ->
|
||||
setFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl))
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}))
|
||||
|
||||
@promptSpies = jasmine.createSpyObj('Prompt.Warning', ["constructor", "show", "hide"])
|
||||
@promptSpies.constructor.and.returnValue(@promptSpies)
|
||||
@promptSpies.show.and.returnValue(@promptSpies)
|
||||
|
||||
@confirmationSpies = jasmine.createSpyObj('Notification.Confirmation', ["constructor", "show"])
|
||||
@confirmationSpies.constructor.and.returnValue(@confirmationSpies)
|
||||
@confirmationSpies.show.and.returnValue(@confirmationSpies)
|
||||
|
||||
@savingSpies = jasmine.createSpyObj('Notification.Mini', ["constructor", "show", "hide"])
|
||||
@savingSpies.constructor.and.returnValue(@savingSpies)
|
||||
@savingSpies.show.and.returnValue(@savingSpies)
|
||||
|
||||
@injector = new Squire()
|
||||
@injector.mock("common/js/components/views/feedback_prompt", {
|
||||
"Warning": @promptSpies.constructor
|
||||
})
|
||||
@injector.mock("common/js/components/views/feedback_notification", {
|
||||
"Confirmation": @confirmationSpies.constructor,
|
||||
"Mini": @savingSpies.constructor
|
||||
})
|
||||
|
||||
@injector.require ["js/models/asset", "js/collections/asset", "js/views/asset"],
|
||||
(AssetModel, AssetCollection, AssetView) =>
|
||||
@model = new AssetModel
|
||||
display_name: "test asset"
|
||||
url: 'actual_asset_url'
|
||||
portable_url: 'portable_url'
|
||||
date_added: 'date'
|
||||
thumbnail: null
|
||||
id: 'id'
|
||||
spyOn(@model, "destroy").and.callThrough()
|
||||
spyOn(@model, "save").and.callThrough()
|
||||
|
||||
@collection = new AssetCollection([@model])
|
||||
@collection.url = "assets-url"
|
||||
@createAssetView = (test) =>
|
||||
view = new AssetView({model: @model})
|
||||
requests = if test then AjaxHelpers["requests"](test) else null
|
||||
return {view: view, requests: requests}
|
||||
done()
|
||||
|
||||
afterEach ->
|
||||
@injector.clean()
|
||||
@injector.remove()
|
||||
|
||||
describe "Basic", ->
|
||||
it "should render properly", ->
|
||||
{view: @view, requests: requests} = @createAssetView()
|
||||
@view.render()
|
||||
expect(@view.$el).toContainText("test asset")
|
||||
|
||||
it "should pop a delete confirmation when the delete button is clicked", ->
|
||||
{view: @view, requests: requests} = @createAssetView()
|
||||
@view.render().$(".remove-asset-button").click()
|
||||
expect(@promptSpies.constructor).toHaveBeenCalled()
|
||||
ctorOptions = @promptSpies.constructor.calls.mostRecent().args[0]
|
||||
expect(ctorOptions.title).toMatch('Delete File Confirmation')
|
||||
# hasn't actually been removed
|
||||
expect(@model.destroy).not.toHaveBeenCalled()
|
||||
expect(@collection).toContain(@model)
|
||||
|
||||
describe "AJAX", ->
|
||||
it "should destroy itself on confirmation", ->
|
||||
{view: @view, requests: requests} = @createAssetView(this)
|
||||
|
||||
@view.render().$(".remove-asset-button").click()
|
||||
ctorOptions = @promptSpies.constructor.calls.mostRecent().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(@confirmationSpies.constructor).not.toHaveBeenCalled()
|
||||
expect(@collection.contains(@model)).toBeTruthy()
|
||||
# return a success response
|
||||
requests[0].respond(204)
|
||||
expect(@confirmationSpies.constructor).toHaveBeenCalled()
|
||||
expect(@confirmationSpies.show).toHaveBeenCalled()
|
||||
savingOptions = @confirmationSpies.constructor.calls.mostRecent().args[0]
|
||||
expect(savingOptions.title).toMatch("Your file has been deleted.")
|
||||
expect(@collection.contains(@model)).toBeFalsy()
|
||||
|
||||
it "should not destroy itself if server errors", ->
|
||||
{view: @view, requests: requests} = @createAssetView(this)
|
||||
|
||||
@view.render().$(".remove-asset-button").click()
|
||||
ctorOptions = @promptSpies.constructor.calls.mostRecent().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()
|
||||
# return an error response
|
||||
requests[0].respond(404)
|
||||
expect(@confirmationSpies.constructor).not.toHaveBeenCalled()
|
||||
expect(@collection.contains(@model)).toBeTruthy()
|
||||
|
||||
it "should lock the asset on confirmation", ->
|
||||
{view: @view, requests: requests} = @createAssetView(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(@savingSpies.constructor).toHaveBeenCalled()
|
||||
expect(@savingSpies.show).toHaveBeenCalled()
|
||||
savingOptions = @savingSpies.constructor.calls.mostRecent().args[0]
|
||||
expect(savingOptions.title).toMatch("Saving")
|
||||
expect(@model.get("locked")).toBeFalsy()
|
||||
# return a success response
|
||||
requests[0].respond(204)
|
||||
expect(@savingSpies.hide).toHaveBeenCalled()
|
||||
expect(@model.get("locked")).toBeTruthy()
|
||||
|
||||
it "should not lock the asset if server errors", ->
|
||||
{view: @view, requests: requests} = @createAssetView(this)
|
||||
|
||||
@view.render().$(".lock-checkbox").click()
|
||||
# return an error response
|
||||
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()
|
||||
|
||||
describe "Assets view", ->
|
||||
beforeEach (done) ->
|
||||
setFixtures($("<script>", {id: "asset-library-tpl", type: "text/template"}).text(assetLibraryTpl))
|
||||
appendSetFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl))
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track'])
|
||||
window.course_location_analytics = jasmine.createSpy()
|
||||
appendSetFixtures(sandbox({id: "asset_table_body"}))
|
||||
|
||||
@promptSpies = jasmine.createSpyObj('Prompt.Warning', ["constructor", "show", "hide"])
|
||||
@promptSpies.constructor.and.returnValue(@promptSpies)
|
||||
@promptSpies.show.and.returnValue(@promptSpies)
|
||||
|
||||
@injector = new Squire()
|
||||
@injector.mock("common/js/components/views/feedback_prompt", {
|
||||
"Warning": @promptSpies.constructor
|
||||
})
|
||||
|
||||
@mockAsset1 = {
|
||||
display_name: "test asset 1"
|
||||
url: 'actual_asset_url_1'
|
||||
portable_url: 'portable_url_1'
|
||||
date_added: 'date_1'
|
||||
thumbnail: null
|
||||
id: 'id_1'
|
||||
}
|
||||
@mockAsset2 = {
|
||||
display_name: "test asset 2"
|
||||
url: 'actual_asset_url_2'
|
||||
portable_url: 'portable_url_2'
|
||||
date_added: 'date_2'
|
||||
thumbnail: null
|
||||
id: 'id_2'
|
||||
}
|
||||
@mockAssetsResponse = {
|
||||
assets: [ @mockAsset1, @mockAsset2 ],
|
||||
start: 0,
|
||||
end: 1,
|
||||
page: 0,
|
||||
pageSize: 5,
|
||||
totalCount: 2
|
||||
}
|
||||
|
||||
@injector.require ["js/models/asset", "js/collections/asset", "js/views/assets"],
|
||||
(AssetModel, AssetCollection, AssetsView) =>
|
||||
@AssetModel = AssetModel
|
||||
@collection = new AssetCollection();
|
||||
@collection.url = "assets-url"
|
||||
@createAssetsView = (test) =>
|
||||
requests = AjaxHelpers.requests(test)
|
||||
view = new AssetsView
|
||||
collection: @collection
|
||||
el: $('#asset_table_body')
|
||||
view.render()
|
||||
return {view: view, requests: requests}
|
||||
done()
|
||||
|
||||
$.ajax()
|
||||
|
||||
afterEach ->
|
||||
delete window.analytics
|
||||
delete window.course_location_analytics
|
||||
|
||||
@injector.clean()
|
||||
@injector.remove()
|
||||
|
||||
addMockAsset = (requests) ->
|
||||
model = new @AssetModel
|
||||
display_name: "new asset"
|
||||
url: 'new_actual_asset_url'
|
||||
portable_url: 'portable_url'
|
||||
date_added: 'date'
|
||||
thumbnail: null
|
||||
id: 'idx'
|
||||
@view.addAsset(model)
|
||||
AjaxHelpers.respondWithJson(requests,
|
||||
{
|
||||
assets: [
|
||||
@mockAsset1, @mockAsset2,
|
||||
{
|
||||
display_name: "new asset"
|
||||
url: 'new_actual_asset_url'
|
||||
portable_url: 'portable_url'
|
||||
date_added: 'date'
|
||||
thumbnail: null
|
||||
id: 'idx'
|
||||
}
|
||||
],
|
||||
start: 0,
|
||||
end: 2,
|
||||
page: 0,
|
||||
pageSize: 5,
|
||||
totalCount: 3
|
||||
})
|
||||
|
||||
|
||||
describe "Basic", ->
|
||||
# Separate setup method to work-around mis-parenting of beforeEach methods
|
||||
setup = (requests) ->
|
||||
@view.pagingView.setPage(1)
|
||||
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
|
||||
|
||||
$.fn.fileupload = ->
|
||||
return ''
|
||||
|
||||
clickEvent = (html_selector) ->
|
||||
$(html_selector).click()
|
||||
|
||||
it "should show upload modal on clicking upload asset button", ->
|
||||
{view: @view, requests: requests} = @createAssetsView(this)
|
||||
spyOn(@view, "showUploadModal")
|
||||
setup.call(this, requests)
|
||||
expect(@view.showUploadModal).not.toHaveBeenCalled()
|
||||
@view.showUploadModal(clickEvent(".upload-button"))
|
||||
expect(@view.showUploadModal).toHaveBeenCalled()
|
||||
|
||||
it "should show file selection menu on choose file button", ->
|
||||
{view: @view, requests: requests} = @createAssetsView(this)
|
||||
spyOn(@view, "showFileSelectionMenu")
|
||||
setup.call(this, requests)
|
||||
expect(@view.showFileSelectionMenu).not.toHaveBeenCalled()
|
||||
@view.showFileSelectionMenu(clickEvent(".choose-file-button"))
|
||||
expect(@view.showFileSelectionMenu).toHaveBeenCalled()
|
||||
|
||||
it "should hide upload modal on clicking close button", ->
|
||||
{view: @view, requests: requests} = @createAssetsView(this)
|
||||
spyOn(@view, "hideModal")
|
||||
setup.call(this, requests)
|
||||
expect(@view.hideModal).not.toHaveBeenCalled()
|
||||
@view.hideModal(clickEvent(".close-button"))
|
||||
expect(@view.hideModal).toHaveBeenCalled()
|
||||
|
||||
it "should show a status indicator while loading", ->
|
||||
{view: @view, requests: requests} = @createAssetsView(this)
|
||||
appendSetFixtures('<div class="ui-loading"/>')
|
||||
expect($('.ui-loading').is(':visible')).toBe(true)
|
||||
setup.call(this, requests)
|
||||
expect($('.ui-loading').is(':visible')).toBe(false)
|
||||
|
||||
it "should hide the status indicator if an error occurs while loading", ->
|
||||
{view: @view, requests: requests} = @createAssetsView(this)
|
||||
appendSetFixtures('<div class="ui-loading"/>')
|
||||
expect($('.ui-loading').is(':visible')).toBe(true)
|
||||
@view.pagingView.setPage(1)
|
||||
AjaxHelpers.respondWithError(requests)
|
||||
expect($('.ui-loading').is(':visible')).toBe(false)
|
||||
|
||||
it "should render both assets", ->
|
||||
{view: @view, requests: requests} = @createAssetsView(this)
|
||||
setup.call(this, requests)
|
||||
expect(@view.$el).toContainText("test asset 1")
|
||||
expect(@view.$el).toContainText("test asset 2")
|
||||
|
||||
it "should remove the deleted asset from the view", ->
|
||||
{view: @view, requests: requests} = @createAssetsView(this)
|
||||
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
|
||||
setup.call(this, requests)
|
||||
# Delete the 2nd asset with success from server.
|
||||
@view.$(".remove-asset-button")[1].click()
|
||||
@promptSpies.constructor.calls.mostRecent().args[0].actions.primary.click(@promptSpies)
|
||||
AjaxHelpers.respondWithNoContent(requests)
|
||||
expect(@view.$el).toContainText("test asset 1")
|
||||
expect(@view.$el).not.toContainText("test asset 2")
|
||||
|
||||
it "does not remove asset if deletion failed", ->
|
||||
{view: @view, requests: requests} = @createAssetsView(this)
|
||||
setup.call(this, requests)
|
||||
# Delete the 2nd asset, but mimic a failure from the server.
|
||||
@view.$(".remove-asset-button")[1].click()
|
||||
@promptSpies.constructor.calls.mostRecent().args[0].actions.primary.click(@promptSpies)
|
||||
AjaxHelpers.respondWithError(requests)
|
||||
expect(@view.$el).toContainText("test asset 1")
|
||||
expect(@view.$el).toContainText("test asset 2")
|
||||
|
||||
it "adds an asset if asset does not already exist", ->
|
||||
{view: @view, requests: requests} = @createAssetsView(this)
|
||||
setup.call(this, requests)
|
||||
addMockAsset.call(this, requests)
|
||||
expect(@view.$el).toContainText("new asset")
|
||||
expect(@collection.models.length).toBe(3)
|
||||
|
||||
it "does not add an asset if asset already exists", ->
|
||||
{view: @view, requests: requests} = @createAssetsView(this)
|
||||
setup.call(this, requests)
|
||||
spyOn(@collection, "add").and.callThrough()
|
||||
model = @collection.models[1]
|
||||
@view.addAsset(model)
|
||||
expect(@collection.add).not.toHaveBeenCalled()
|
||||
|
||||
describe "Sorting", ->
|
||||
# Separate setup method to work-around mis-parenting of beforeEach methods
|
||||
setup = (requests) ->
|
||||
@view.pagingView.setPage(1)
|
||||
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
|
||||
|
||||
it "should have the correct default sort order", ->
|
||||
{view: @view, requests: requests} = @createAssetsView(this)
|
||||
setup.call(this, requests)
|
||||
expect(@view.pagingView.sortDisplayName()).toBe("Date Added")
|
||||
expect(@view.collection.sortDirection).toBe("desc")
|
||||
|
||||
it "should toggle the sort order when clicking on the currently sorted column", ->
|
||||
{view: @view, requests: requests} = @createAssetsView(this)
|
||||
setup.call(this, requests)
|
||||
expect(@view.pagingView.sortDisplayName()).toBe("Date Added")
|
||||
expect(@view.collection.sortDirection).toBe("desc")
|
||||
@view.$("#js-asset-date-col").click()
|
||||
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
|
||||
expect(@view.pagingView.sortDisplayName()).toBe("Date Added")
|
||||
expect(@view.collection.sortDirection).toBe("asc")
|
||||
@view.$("#js-asset-date-col").click()
|
||||
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
|
||||
expect(@view.pagingView.sortDisplayName()).toBe("Date Added")
|
||||
expect(@view.collection.sortDirection).toBe("desc")
|
||||
|
||||
it "should switch the sort order when clicking on a different column", ->
|
||||
{view: @view, requests: requests} = @createAssetsView(this)
|
||||
setup.call(this, requests)
|
||||
@view.$("#js-asset-name-col").click()
|
||||
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
|
||||
expect(@view.pagingView.sortDisplayName()).toBe("Name")
|
||||
expect(@view.collection.sortDirection).toBe("asc")
|
||||
@view.$("#js-asset-name-col").click()
|
||||
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
|
||||
expect(@view.pagingView.sortDisplayName()).toBe("Name")
|
||||
expect(@view.collection.sortDirection).toBe("desc")
|
||||
|
||||
it "should switch sort to most recent date added when a new asset is added", ->
|
||||
{view: @view, requests: requests} = @createAssetsView(this)
|
||||
setup.call(this, requests)
|
||||
@view.$("#js-asset-name-col").click()
|
||||
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
|
||||
addMockAsset.call(this, requests)
|
||||
AjaxHelpers.respondWithJson(requests, @mockAssetsResponse)
|
||||
expect(@view.pagingView.sortDisplayName()).toBe("Date Added")
|
||||
expect(@view.collection.sortDirection).toBe("desc")
|
||||
428
cms/static/coffee/spec/views/assets_spec.js
Normal file
428
cms/static/coffee/spec/views/assets_spec.js
Normal file
@@ -0,0 +1,428 @@
|
||||
define(["jquery", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "squire"],
|
||||
function($, AjaxHelpers, Squire) {
|
||||
|
||||
const assetLibraryTpl = readFixtures('asset-library.underscore');
|
||||
const assetTpl = readFixtures('asset.underscore');
|
||||
|
||||
describe("Asset view", function() {
|
||||
beforeEach(function(done) {
|
||||
setFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl));
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}));
|
||||
|
||||
this.promptSpies = jasmine.createSpyObj('Prompt.Warning', ["constructor", "show", "hide"]);
|
||||
this.promptSpies.constructor.and.returnValue(this.promptSpies);
|
||||
this.promptSpies.show.and.returnValue(this.promptSpies);
|
||||
|
||||
this.confirmationSpies = jasmine.createSpyObj('Notification.Confirmation', ["constructor", "show"]);
|
||||
this.confirmationSpies.constructor.and.returnValue(this.confirmationSpies);
|
||||
this.confirmationSpies.show.and.returnValue(this.confirmationSpies);
|
||||
|
||||
this.savingSpies = jasmine.createSpyObj('Notification.Mini', ["constructor", "show", "hide"]);
|
||||
this.savingSpies.constructor.and.returnValue(this.savingSpies);
|
||||
this.savingSpies.show.and.returnValue(this.savingSpies);
|
||||
|
||||
this.injector = new Squire();
|
||||
this.injector.mock("common/js/components/views/feedback_prompt", {
|
||||
"Warning": this.promptSpies.constructor
|
||||
});
|
||||
this.injector.mock("common/js/components/views/feedback_notification", {
|
||||
"Confirmation": this.confirmationSpies.constructor,
|
||||
"Mini": this.savingSpies.constructor
|
||||
});
|
||||
|
||||
this.injector.require(["js/models/asset", "js/collections/asset", "js/views/asset"],
|
||||
(AssetModel, AssetCollection, AssetView) => {
|
||||
this.model = new AssetModel({
|
||||
display_name: "test asset",
|
||||
url: 'actual_asset_url',
|
||||
portable_url: 'portable_url',
|
||||
date_added: 'date',
|
||||
thumbnail: null,
|
||||
id: 'id'
|
||||
});
|
||||
spyOn(this.model, "destroy").and.callThrough();
|
||||
spyOn(this.model, "save").and.callThrough();
|
||||
|
||||
this.collection = new AssetCollection([this.model]);
|
||||
this.collection.url = "assets-url";
|
||||
this.createAssetView = test => {
|
||||
const view = new AssetView({model: this.model});
|
||||
const requests = test ? AjaxHelpers["requests"](test) : null;
|
||||
return {view, requests};
|
||||
};
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
this.injector.clean();
|
||||
this.injector.remove();
|
||||
});
|
||||
|
||||
describe("Basic", function() {
|
||||
it("should render properly", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetView());
|
||||
this.view.render();
|
||||
expect(this.view.$el).toContainText("test asset");
|
||||
});
|
||||
|
||||
it("should pop a delete confirmation when the delete button is clicked", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetView());
|
||||
this.view.render().$(".remove-asset-button").click();
|
||||
expect(this.promptSpies.constructor).toHaveBeenCalled();
|
||||
const ctorOptions = this.promptSpies.constructor.calls.mostRecent().args[0];
|
||||
expect(ctorOptions.title).toMatch('Delete File Confirmation');
|
||||
// hasn't actually been removed
|
||||
expect(this.model.destroy).not.toHaveBeenCalled();
|
||||
expect(this.collection).toContain(this.model);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AJAX", function() {
|
||||
it("should destroy itself on confirmation", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetView(this));
|
||||
|
||||
this.view.render().$(".remove-asset-button").click();
|
||||
const ctorOptions = this.promptSpies.constructor.calls.mostRecent().args[0];
|
||||
// run the primary function to indicate confirmation
|
||||
ctorOptions.actions.primary.click(this.promptSpies);
|
||||
// AJAX request has been sent, but not yet returned
|
||||
expect(this.model.destroy).toHaveBeenCalled();
|
||||
expect(requests.length).toEqual(1);
|
||||
expect(this.confirmationSpies.constructor).not.toHaveBeenCalled();
|
||||
expect(this.collection.contains(this.model)).toBeTruthy();
|
||||
// return a success response
|
||||
requests[0].respond(204);
|
||||
expect(this.confirmationSpies.constructor).toHaveBeenCalled();
|
||||
expect(this.confirmationSpies.show).toHaveBeenCalled();
|
||||
const savingOptions = this.confirmationSpies.constructor.calls.mostRecent().args[0];
|
||||
expect(savingOptions.title).toMatch("Your file has been deleted.");
|
||||
expect(this.collection.contains(this.model)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not destroy itself if server errors", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetView(this));
|
||||
|
||||
this.view.render().$(".remove-asset-button").click();
|
||||
const ctorOptions = this.promptSpies.constructor.calls.mostRecent().args[0];
|
||||
// run the primary function to indicate confirmation
|
||||
ctorOptions.actions.primary.click(this.promptSpies);
|
||||
// AJAX request has been sent, but not yet returned
|
||||
expect(this.model.destroy).toHaveBeenCalled();
|
||||
// return an error response
|
||||
requests[0].respond(404);
|
||||
expect(this.confirmationSpies.constructor).not.toHaveBeenCalled();
|
||||
expect(this.collection.contains(this.model)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should lock the asset on confirmation", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetView(this));
|
||||
|
||||
this.view.render().$(".lock-checkbox").click();
|
||||
// AJAX request has been sent, but not yet returned
|
||||
expect(this.model.save).toHaveBeenCalled();
|
||||
expect(requests.length).toEqual(1);
|
||||
expect(this.savingSpies.constructor).toHaveBeenCalled();
|
||||
expect(this.savingSpies.show).toHaveBeenCalled();
|
||||
const savingOptions = this.savingSpies.constructor.calls.mostRecent().args[0];
|
||||
expect(savingOptions.title).toMatch("Saving");
|
||||
expect(this.model.get("locked")).toBeFalsy();
|
||||
// return a success response
|
||||
requests[0].respond(204);
|
||||
expect(this.savingSpies.hide).toHaveBeenCalled();
|
||||
expect(this.model.get("locked")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not lock the asset if server errors", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetView(this));
|
||||
|
||||
this.view.render().$(".lock-checkbox").click();
|
||||
// return an error response
|
||||
requests[0].respond(404);
|
||||
// Don't call hide because that closes the notification showing the server error.
|
||||
expect(this.savingSpies.hide).not.toHaveBeenCalled();
|
||||
expect(this.model.get("locked")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Assets view", function() {
|
||||
beforeEach(function(done) {
|
||||
setFixtures($("<script>", {id: "asset-library-tpl", type: "text/template"}).text(assetLibraryTpl));
|
||||
appendSetFixtures($("<script>", {id: "asset-tpl", type: "text/template"}).text(assetTpl));
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track']);
|
||||
window.course_location_analytics = jasmine.createSpy();
|
||||
appendSetFixtures(sandbox({id: "asset_table_body"}));
|
||||
|
||||
this.promptSpies = jasmine.createSpyObj('Prompt.Warning', ["constructor", "show", "hide"]);
|
||||
this.promptSpies.constructor.and.returnValue(this.promptSpies);
|
||||
this.promptSpies.show.and.returnValue(this.promptSpies);
|
||||
|
||||
this.injector = new Squire();
|
||||
this.injector.mock("common/js/components/views/feedback_prompt", {
|
||||
"Warning": this.promptSpies.constructor
|
||||
});
|
||||
|
||||
this.mockAsset1 = {
|
||||
display_name: "test asset 1",
|
||||
url: 'actual_asset_url_1',
|
||||
portable_url: 'portable_url_1',
|
||||
date_added: 'date_1',
|
||||
thumbnail: null,
|
||||
id: 'id_1'
|
||||
};
|
||||
this.mockAsset2 = {
|
||||
display_name: "test asset 2",
|
||||
url: 'actual_asset_url_2',
|
||||
portable_url: 'portable_url_2',
|
||||
date_added: 'date_2',
|
||||
thumbnail: null,
|
||||
id: 'id_2'
|
||||
};
|
||||
this.mockAssetsResponse = {
|
||||
assets: [ this.mockAsset1, this.mockAsset2 ],
|
||||
start: 0,
|
||||
end: 1,
|
||||
page: 0,
|
||||
pageSize: 5,
|
||||
totalCount: 2
|
||||
};
|
||||
|
||||
this.injector.require(["js/models/asset", "js/collections/asset", "js/views/assets"],
|
||||
(AssetModel, AssetCollection, AssetsView) => {
|
||||
this.AssetModel = AssetModel;
|
||||
this.collection = new AssetCollection();
|
||||
this.collection.url = "assets-url";
|
||||
this.createAssetsView = test => {
|
||||
const requests = AjaxHelpers.requests(test);
|
||||
const view = new AssetsView({
|
||||
collection: this.collection,
|
||||
el: $('#asset_table_body')
|
||||
});
|
||||
view.render();
|
||||
return {view, requests};
|
||||
};
|
||||
return done();
|
||||
});
|
||||
|
||||
return $.ajax();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
delete window.analytics;
|
||||
delete window.course_location_analytics;
|
||||
|
||||
this.injector.clean();
|
||||
this.injector.remove();
|
||||
});
|
||||
|
||||
const addMockAsset = function(requests) {
|
||||
const model = new this.AssetModel({
|
||||
display_name: "new asset",
|
||||
url: 'new_actual_asset_url',
|
||||
portable_url: 'portable_url',
|
||||
date_added: 'date',
|
||||
thumbnail: null,
|
||||
id: 'idx'
|
||||
});
|
||||
this.view.addAsset(model);
|
||||
return AjaxHelpers.respondWithJson(requests,
|
||||
{
|
||||
assets: [
|
||||
this.mockAsset1, this.mockAsset2,
|
||||
{
|
||||
display_name: "new asset",
|
||||
url: 'new_actual_asset_url',
|
||||
portable_url: 'portable_url',
|
||||
date_added: 'date',
|
||||
thumbnail: null,
|
||||
id: 'idx'
|
||||
}
|
||||
],
|
||||
start: 0,
|
||||
end: 2,
|
||||
page: 0,
|
||||
pageSize: 5,
|
||||
totalCount: 3
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
describe("Basic", function() {
|
||||
// Separate setup method to work-around mis-parenting of beforeEach methods
|
||||
const setup = function(requests) {
|
||||
this.view.pagingView.setPage(1);
|
||||
return AjaxHelpers.respondWithJson(requests, this.mockAssetsResponse);
|
||||
};
|
||||
|
||||
$.fn.fileupload = () => '';
|
||||
|
||||
const clickEvent = html_selector => $(html_selector).click();
|
||||
|
||||
it("should show upload modal on clicking upload asset button", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetsView(this));
|
||||
spyOn(this.view, "showUploadModal");
|
||||
setup.call(this, requests);
|
||||
expect(this.view.showUploadModal).not.toHaveBeenCalled();
|
||||
this.view.showUploadModal(clickEvent(".upload-button"));
|
||||
expect(this.view.showUploadModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show file selection menu on choose file button", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetsView(this));
|
||||
spyOn(this.view, "showFileSelectionMenu");
|
||||
setup.call(this, requests);
|
||||
expect(this.view.showFileSelectionMenu).not.toHaveBeenCalled();
|
||||
this.view.showFileSelectionMenu(clickEvent(".choose-file-button"));
|
||||
expect(this.view.showFileSelectionMenu).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should hide upload modal on clicking close button", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetsView(this));
|
||||
spyOn(this.view, "hideModal");
|
||||
setup.call(this, requests);
|
||||
expect(this.view.hideModal).not.toHaveBeenCalled();
|
||||
this.view.hideModal(clickEvent(".close-button"));
|
||||
expect(this.view.hideModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show a status indicator while loading", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetsView(this));
|
||||
appendSetFixtures('<div class="ui-loading"/>');
|
||||
expect($('.ui-loading').is(':visible')).toBe(true);
|
||||
setup.call(this, requests);
|
||||
expect($('.ui-loading').is(':visible')).toBe(false);
|
||||
});
|
||||
|
||||
it("should hide the status indicator if an error occurs while loading", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetsView(this));
|
||||
appendSetFixtures('<div class="ui-loading"/>');
|
||||
expect($('.ui-loading').is(':visible')).toBe(true);
|
||||
this.view.pagingView.setPage(1);
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
expect($('.ui-loading').is(':visible')).toBe(false);
|
||||
});
|
||||
|
||||
it("should render both assets", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetsView(this));
|
||||
setup.call(this, requests);
|
||||
expect(this.view.$el).toContainText("test asset 1");
|
||||
expect(this.view.$el).toContainText("test asset 2");
|
||||
});
|
||||
|
||||
it("should remove the deleted asset from the view", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetsView(this));
|
||||
AjaxHelpers.respondWithJson(requests, this.mockAssetsResponse);
|
||||
setup.call(this, requests);
|
||||
// Delete the 2nd asset with success from server.
|
||||
this.view.$(".remove-asset-button")[1].click();
|
||||
this.promptSpies.constructor.calls.mostRecent().args[0].actions.primary.click(this.promptSpies);
|
||||
AjaxHelpers.respondWithNoContent(requests);
|
||||
expect(this.view.$el).toContainText("test asset 1");
|
||||
expect(this.view.$el).not.toContainText("test asset 2");
|
||||
});
|
||||
|
||||
it("does not remove asset if deletion failed", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetsView(this));
|
||||
setup.call(this, requests);
|
||||
// Delete the 2nd asset, but mimic a failure from the server.
|
||||
this.view.$(".remove-asset-button")[1].click();
|
||||
this.promptSpies.constructor.calls.mostRecent().args[0].actions.primary.click(this.promptSpies);
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
expect(this.view.$el).toContainText("test asset 1");
|
||||
expect(this.view.$el).toContainText("test asset 2");
|
||||
});
|
||||
|
||||
it("adds an asset if asset does not already exist", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetsView(this));
|
||||
setup.call(this, requests);
|
||||
addMockAsset.call(this, requests);
|
||||
expect(this.view.$el).toContainText("new asset");
|
||||
expect(this.collection.models.length).toBe(3);
|
||||
});
|
||||
|
||||
it("does not add an asset if asset already exists", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetsView(this));
|
||||
setup.call(this, requests);
|
||||
spyOn(this.collection, "add").and.callThrough();
|
||||
const model = this.collection.models[1];
|
||||
this.view.addAsset(model);
|
||||
expect(this.collection.add).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sorting", function() {
|
||||
// Separate setup method to work-around mis-parenting of beforeEach methods
|
||||
const setup = function(requests) {
|
||||
this.view.pagingView.setPage(1);
|
||||
return AjaxHelpers.respondWithJson(requests, this.mockAssetsResponse);
|
||||
};
|
||||
|
||||
it("should have the correct default sort order", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetsView(this));
|
||||
setup.call(this, requests);
|
||||
expect(this.view.pagingView.sortDisplayName()).toBe("Date Added");
|
||||
expect(this.view.collection.sortDirection).toBe("desc");
|
||||
});
|
||||
|
||||
it("should toggle the sort order when clicking on the currently sorted column", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetsView(this));
|
||||
setup.call(this, requests);
|
||||
expect(this.view.pagingView.sortDisplayName()).toBe("Date Added");
|
||||
expect(this.view.collection.sortDirection).toBe("desc");
|
||||
this.view.$("#js-asset-date-col").click();
|
||||
AjaxHelpers.respondWithJson(requests, this.mockAssetsResponse);
|
||||
expect(this.view.pagingView.sortDisplayName()).toBe("Date Added");
|
||||
expect(this.view.collection.sortDirection).toBe("asc");
|
||||
this.view.$("#js-asset-date-col").click();
|
||||
AjaxHelpers.respondWithJson(requests, this.mockAssetsResponse);
|
||||
expect(this.view.pagingView.sortDisplayName()).toBe("Date Added");
|
||||
expect(this.view.collection.sortDirection).toBe("desc");
|
||||
});
|
||||
|
||||
it("should switch the sort order when clicking on a different column", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetsView(this));
|
||||
setup.call(this, requests);
|
||||
this.view.$("#js-asset-name-col").click();
|
||||
AjaxHelpers.respondWithJson(requests, this.mockAssetsResponse);
|
||||
expect(this.view.pagingView.sortDisplayName()).toBe("Name");
|
||||
expect(this.view.collection.sortDirection).toBe("asc");
|
||||
this.view.$("#js-asset-name-col").click();
|
||||
AjaxHelpers.respondWithJson(requests, this.mockAssetsResponse);
|
||||
expect(this.view.pagingView.sortDisplayName()).toBe("Name");
|
||||
expect(this.view.collection.sortDirection).toBe("desc");
|
||||
});
|
||||
|
||||
it("should switch sort to most recent date added when a new asset is added", function() {
|
||||
let requests;
|
||||
({view: this.view, requests} = this.createAssetsView(this));
|
||||
setup.call(this, requests);
|
||||
this.view.$("#js-asset-name-col").click();
|
||||
AjaxHelpers.respondWithJson(requests, this.mockAssetsResponse);
|
||||
addMockAsset.call(this, requests);
|
||||
AjaxHelpers.respondWithJson(requests, this.mockAssetsResponse);
|
||||
expect(this.view.pagingView.sortDisplayName()).toBe("Date Added");
|
||||
expect(this.view.collection.sortDirection).toBe("desc");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,298 +0,0 @@
|
||||
define ["js/views/course_info_handout", "js/views/course_info_update", "js/models/module_info",
|
||||
"js/collections/course_update", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers"],
|
||||
(CourseInfoHandoutsView, CourseInfoUpdateView, ModuleInfo, CourseUpdateCollection, AjaxHelpers) ->
|
||||
|
||||
describe "Course Updates and Handouts", ->
|
||||
courseInfoPage = """
|
||||
<div class="course-info-wrapper">
|
||||
<div class="main-column window">
|
||||
<article class="course-updates" id="course-update-view">
|
||||
<ol class="update-list" id="course-update-list"></ol>
|
||||
</article>
|
||||
</div>
|
||||
<div class="sidebar window course-handouts" id="course-handouts-view"></div>
|
||||
</div>
|
||||
<div class="modal-cover"></div>
|
||||
"""
|
||||
|
||||
beforeEach ->
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track'])
|
||||
window.course_location_analytics = jasmine.createSpy()
|
||||
|
||||
afterEach ->
|
||||
delete window.analytics
|
||||
delete window.course_location_analytics
|
||||
|
||||
describe "Course Updates without Push notification", ->
|
||||
courseInfoTemplate = readFixtures('course_info_update.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "course_info_update-tpl", type: "text/template"}).text(courseInfoTemplate))
|
||||
appendSetFixtures courseInfoPage
|
||||
|
||||
@collection = new CourseUpdateCollection()
|
||||
@collection.url = 'course_info_update/'
|
||||
@courseInfoEdit = new CourseInfoUpdateView({
|
||||
el: $('.course-updates'),
|
||||
collection: @collection,
|
||||
base_asset_url : 'base-asset-url/'
|
||||
})
|
||||
|
||||
@courseInfoEdit.render()
|
||||
|
||||
@event = {
|
||||
preventDefault : () -> 'no op'
|
||||
}
|
||||
|
||||
@createNewUpdate = (text) ->
|
||||
# Edit button is not in the template under test (it is in parent HTML).
|
||||
# Therefore call onNew directly.
|
||||
@courseInfoEdit.onNew(@event)
|
||||
spyOn(@courseInfoEdit.$codeMirror, 'getValue').and.returnValue(text)
|
||||
@courseInfoEdit.$el.find('.save-button').click()
|
||||
|
||||
@cancelNewCourseInfo = (useCancelButton) ->
|
||||
@courseInfoEdit.onNew(@event)
|
||||
spyOn(@courseInfoEdit.$modalCover, 'hide').and.callThrough()
|
||||
|
||||
spyOn(@courseInfoEdit.$codeMirror, 'getValue').and.returnValue('unsaved changes')
|
||||
model = @collection.at(0)
|
||||
spyOn(model, "save").and.callThrough()
|
||||
|
||||
cancelEditingUpdate(@courseInfoEdit, @courseInfoEdit.$modalCover, useCancelButton)
|
||||
|
||||
expect(@courseInfoEdit.$modalCover.hide).toHaveBeenCalled()
|
||||
expect(model.save).not.toHaveBeenCalled()
|
||||
previewContents = @courseInfoEdit.$el.find('.update-contents').html()
|
||||
expect(previewContents).not.toEqual('unsaved changes')
|
||||
|
||||
@doNotCloseNewCourseInfo = () ->
|
||||
@courseInfoEdit.onNew(@event)
|
||||
spyOn(@courseInfoEdit.$modalCover, 'hide').and.callThrough()
|
||||
|
||||
spyOn(@courseInfoEdit.$codeMirror, 'getValue').and.returnValue('unsaved changes')
|
||||
model = @collection.at(0)
|
||||
spyOn(model, "save").and.callThrough()
|
||||
|
||||
cancelEditingUpdate(@courseInfoEdit, @courseInfoEdit.$modalCover, false)
|
||||
|
||||
expect(model.save).not.toHaveBeenCalled()
|
||||
expect(@courseInfoEdit.$modalCover.hide).not.toHaveBeenCalled()
|
||||
|
||||
@cancelExistingCourseInfo = (useCancelButton) ->
|
||||
@createNewUpdate('existing update')
|
||||
@courseInfoEdit.$el.find('.edit-button').click()
|
||||
spyOn(@courseInfoEdit.$modalCover, 'hide').and.callThrough()
|
||||
|
||||
spyOn(@courseInfoEdit.$codeMirror, 'getValue').and.returnValue('modification')
|
||||
model = @collection.at(0)
|
||||
spyOn(model, "save").and.callThrough()
|
||||
model.id = "saved_to_server"
|
||||
cancelEditingUpdate(@courseInfoEdit, @courseInfoEdit.$modalCover, useCancelButton)
|
||||
|
||||
expect(@courseInfoEdit.$modalCover.hide).toHaveBeenCalled()
|
||||
expect(model.save).not.toHaveBeenCalled()
|
||||
previewContents = @courseInfoEdit.$el.find('.update-contents').html()
|
||||
expect(previewContents).toEqual('existing update')
|
||||
|
||||
@testInvalidDateValue = (value) ->
|
||||
@courseInfoEdit.onNew(@event)
|
||||
expect(@courseInfoEdit.$el.find('.save-button').hasClass("is-disabled")).toEqual(false)
|
||||
@courseInfoEdit.$el.find('input.date').val(value).trigger("change")
|
||||
expect(@courseInfoEdit.$el.find('.save-button').hasClass("is-disabled")).toEqual(true)
|
||||
@courseInfoEdit.$el.find('input.date').val("01/01/16").trigger("change")
|
||||
expect(@courseInfoEdit.$el.find('.save-button').hasClass("is-disabled")).toEqual(false)
|
||||
|
||||
cancelEditingUpdate = (update, modalCover, useCancelButton) ->
|
||||
if useCancelButton
|
||||
update.$el.find('.cancel-button').click()
|
||||
else
|
||||
modalCover.click()
|
||||
|
||||
it "does send expected data on save", ->
|
||||
requests = AjaxHelpers["requests"](this)
|
||||
|
||||
# Create a new update, verifying that the model is created
|
||||
# in the collection and save is called.
|
||||
expect(@collection.isEmpty()).toBeTruthy()
|
||||
@courseInfoEdit.onNew(@event)
|
||||
expect(@collection.length).toEqual(1)
|
||||
model = @collection.at(0)
|
||||
spyOn(model, "save").and.callThrough()
|
||||
spyOn(@courseInfoEdit.$codeMirror, 'getValue').and.returnValue('/static/image.jpg')
|
||||
|
||||
# Click the "Save button."
|
||||
@courseInfoEdit.$el.find('.save-button').click()
|
||||
expect(model.save).toHaveBeenCalled()
|
||||
|
||||
# Verify push_notification_selected is set to false.
|
||||
requestSent = JSON.parse(requests[requests.length - 1].requestBody)
|
||||
expect(requestSent.push_notification_selected).toEqual(false)
|
||||
|
||||
# Verify the link is not rewritten when saved.
|
||||
expect(requestSent.content).toEqual('/static/image.jpg')
|
||||
|
||||
# Verify that analytics are sent
|
||||
expect(window.analytics.track).toHaveBeenCalled()
|
||||
|
||||
it "does rewrite links for preview", ->
|
||||
# Create a new update.
|
||||
@createNewUpdate('/static/image.jpg')
|
||||
|
||||
# Verify the link is rewritten for preview purposes.
|
||||
previewContents = @courseInfoEdit.$el.find('.update-contents').html()
|
||||
expect(previewContents).toEqual('base-asset-url/image.jpg')
|
||||
|
||||
it "shows static links in edit mode", ->
|
||||
@createNewUpdate('/static/image.jpg')
|
||||
|
||||
# Click edit and verify CodeMirror contents.
|
||||
@courseInfoEdit.$el.find('.edit-button').click()
|
||||
expect(@courseInfoEdit.$codeMirror.getValue()).toEqual('/static/image.jpg')
|
||||
|
||||
it "removes newly created course info on cancel", ->
|
||||
@cancelNewCourseInfo(true)
|
||||
|
||||
it "do not close new course info on click outside modal", ->
|
||||
@doNotCloseNewCourseInfo()
|
||||
|
||||
it "does not remove existing course info on cancel", ->
|
||||
@cancelExistingCourseInfo(true)
|
||||
|
||||
it "does not remove existing course info on click outside modal", ->
|
||||
@cancelExistingCourseInfo(false)
|
||||
|
||||
it "does not allow updates to be saved with an invalid date", ->
|
||||
@testInvalidDateValue("Marchtober 40, 2048")
|
||||
|
||||
it "does not allow updates to be saved with a blank date", ->
|
||||
@testInvalidDateValue("")
|
||||
|
||||
|
||||
describe "Course Updates WITH Push notification", ->
|
||||
courseInfoTemplate = readFixtures('course_info_update.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "course_info_update-tpl", type: "text/template"}).text(courseInfoTemplate))
|
||||
appendSetFixtures courseInfoPage
|
||||
@collection = new CourseUpdateCollection()
|
||||
@collection.url = 'course_info_update/'
|
||||
@courseInfoEdit = new CourseInfoUpdateView({
|
||||
el: $('.course-updates'),
|
||||
collection: @collection,
|
||||
base_asset_url : 'base-asset-url/',
|
||||
push_notification_enabled : true
|
||||
})
|
||||
@courseInfoEdit.render()
|
||||
@event = {preventDefault : () -> 'no op'}
|
||||
@courseInfoEdit.onNew(@event)
|
||||
|
||||
it "shows push notification checkbox as selected by default", ->
|
||||
expect(@courseInfoEdit.$el.find('.toggle-checkbox')).toBeChecked()
|
||||
|
||||
it "sends correct default value for push_notification_selected", ->
|
||||
requests = AjaxHelpers.requests(this);
|
||||
@courseInfoEdit.$el.find('.save-button').click()
|
||||
requestSent = JSON.parse(requests[requests.length - 1].requestBody)
|
||||
expect(requestSent.push_notification_selected).toEqual(true)
|
||||
|
||||
# Check that analytics send push_notification info
|
||||
analytics_payload = window.analytics.track.calls.first().args[1]
|
||||
expect(analytics_payload).toEqual(jasmine.objectContaining({'push_notification_selected': true}))
|
||||
|
||||
it "sends correct value for push_notification_selected when it is unselected", ->
|
||||
requests = AjaxHelpers.requests(this);
|
||||
# unselect push notification
|
||||
@courseInfoEdit.$el.find('.toggle-checkbox').attr('checked', false);
|
||||
@courseInfoEdit.$el.find('.save-button').click()
|
||||
requestSent = JSON.parse(requests[requests.length - 1].requestBody)
|
||||
expect(requestSent.push_notification_selected).toEqual(false)
|
||||
|
||||
# Check that analytics send push_notification info
|
||||
analytics_payload = window.analytics.track.calls.first().args[1]
|
||||
expect(analytics_payload).toEqual(jasmine.objectContaining({'push_notification_selected': false}))
|
||||
|
||||
describe "Course Handouts", ->
|
||||
handoutsTemplate = readFixtures('course_info_handouts.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "course_info_handouts-tpl", type: "text/template"}).text(handoutsTemplate))
|
||||
appendSetFixtures courseInfoPage
|
||||
|
||||
@model = new ModuleInfo({
|
||||
id: 'handouts-id',
|
||||
data: '/static/fromServer.jpg'
|
||||
})
|
||||
|
||||
@handoutsEdit = new CourseInfoHandoutsView({
|
||||
el: $('#course-handouts-view'),
|
||||
model: @model,
|
||||
base_asset_url: 'base-asset-url/'
|
||||
});
|
||||
|
||||
@handoutsEdit.render()
|
||||
|
||||
it "saves <ol></ol> when content left empty", ->
|
||||
requests = AjaxHelpers["requests"](this)
|
||||
|
||||
# Enter empty string in the handouts section, verifying that the model
|
||||
# is saved with '<ol></ol>' instead of the empty string
|
||||
@handoutsEdit.$el.find('.edit-button').click()
|
||||
spyOn(@handoutsEdit.$codeMirror, 'getValue').and.returnValue('')
|
||||
spyOn(@model, "save").and.callThrough()
|
||||
@handoutsEdit.$el.find('.save-button').click()
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
contentSaved = JSON.parse(requests[requests.length - 1].requestBody).data
|
||||
expect(contentSaved).toEqual('<ol></ol>')
|
||||
|
||||
it "does not rewrite links on save", ->
|
||||
requests = AjaxHelpers["requests"](this)
|
||||
|
||||
# Enter something in the handouts section, verifying that the model is saved
|
||||
# when "Save" is clicked.
|
||||
@handoutsEdit.$el.find('.edit-button').click()
|
||||
spyOn(@handoutsEdit.$codeMirror, 'getValue').and.returnValue('/static/image.jpg')
|
||||
spyOn(@model, "save").and.callThrough()
|
||||
@handoutsEdit.$el.find('.save-button').click()
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
contentSaved = JSON.parse(requests[requests.length - 1].requestBody).data
|
||||
expect(contentSaved).toEqual('/static/image.jpg')
|
||||
|
||||
it "does rewrite links in initial content", ->
|
||||
expect(@handoutsEdit.$preview.html().trim()).toBe('base-asset-url/fromServer.jpg')
|
||||
|
||||
it "does rewrite links after edit", ->
|
||||
# Edit handouts and save.
|
||||
@handoutsEdit.$el.find('.edit-button').click()
|
||||
spyOn(@handoutsEdit.$codeMirror, 'getValue').and.returnValue('/static/image.jpg')
|
||||
@handoutsEdit.$el.find('.save-button').click()
|
||||
|
||||
# Verify preview text.
|
||||
expect(@handoutsEdit.$preview.html().trim()).toBe('base-asset-url/image.jpg')
|
||||
|
||||
it "shows static links in edit mode", ->
|
||||
# Click edit and verify CodeMirror contents.
|
||||
@handoutsEdit.$el.find('.edit-button').click()
|
||||
expect(@handoutsEdit.$codeMirror.getValue().trim()).toEqual('/static/fromServer.jpg')
|
||||
|
||||
it "can open course handouts with bad html on edit", ->
|
||||
# Enter some bad html in handouts section, verifying that the
|
||||
# model/handoutform opens when "Edit" is clicked
|
||||
|
||||
@model = new ModuleInfo({
|
||||
id: 'handouts-id',
|
||||
data: '<p><a href="[URL OF FILE]>[LINK TEXT]</a></p>'
|
||||
})
|
||||
@handoutsEdit = new CourseInfoHandoutsView({
|
||||
el: $('#course-handouts-view'),
|
||||
model: @model,
|
||||
base_asset_url: 'base-asset-url/'
|
||||
});
|
||||
@handoutsEdit.render()
|
||||
|
||||
expect($('.edit-handouts-form').is(':hidden')).toEqual(true)
|
||||
@handoutsEdit.$el.find('.edit-button').click()
|
||||
expect(@handoutsEdit.$codeMirror.getValue()).toEqual('<p><a href="[URL OF FILE]>[LINK TEXT]</a></p>')
|
||||
expect($('.edit-handouts-form').is(':hidden')).toEqual(false)
|
||||
334
cms/static/coffee/spec/views/course_info_spec.js
Normal file
334
cms/static/coffee/spec/views/course_info_spec.js
Normal file
@@ -0,0 +1,334 @@
|
||||
define(["js/views/course_info_handout", "js/views/course_info_update", "js/models/module_info",
|
||||
"js/collections/course_update", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers"],
|
||||
(CourseInfoHandoutsView, CourseInfoUpdateView, ModuleInfo, CourseUpdateCollection, AjaxHelpers) =>
|
||||
|
||||
describe("Course Updates and Handouts", function() {
|
||||
const courseInfoPage = `\
|
||||
<div class="course-info-wrapper">
|
||||
<div class="main-column window">
|
||||
<article class="course-updates" id="course-update-view">
|
||||
<ol class="update-list" id="course-update-list"></ol>
|
||||
</article>
|
||||
</div>
|
||||
<div class="sidebar window course-handouts" id="course-handouts-view"></div>
|
||||
</div>
|
||||
<div class="modal-cover"></div>\
|
||||
`;
|
||||
|
||||
beforeEach(function() {
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track']);
|
||||
window.course_location_analytics = jasmine.createSpy();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
delete window.analytics;
|
||||
delete window.course_location_analytics;
|
||||
});
|
||||
|
||||
describe("Course Updates without Push notification", function() {
|
||||
const courseInfoTemplate = readFixtures('course_info_update.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
let cancelEditingUpdate;
|
||||
setFixtures($("<script>", {id: "course_info_update-tpl", type: "text/template"}).text(courseInfoTemplate));
|
||||
appendSetFixtures(courseInfoPage);
|
||||
|
||||
this.collection = new CourseUpdateCollection();
|
||||
this.collection.url = 'course_info_update/';
|
||||
this.courseInfoEdit = new CourseInfoUpdateView({
|
||||
el: $('.course-updates'),
|
||||
collection: this.collection,
|
||||
base_asset_url : 'base-asset-url/'
|
||||
});
|
||||
|
||||
this.courseInfoEdit.render();
|
||||
|
||||
this.event = {
|
||||
preventDefault() { return 'no op'; }
|
||||
};
|
||||
|
||||
this.createNewUpdate = function(text) {
|
||||
// Edit button is not in the template under test (it is in parent HTML).
|
||||
// Therefore call onNew directly.
|
||||
this.courseInfoEdit.onNew(this.event);
|
||||
spyOn(this.courseInfoEdit.$codeMirror, 'getValue').and.returnValue(text);
|
||||
return this.courseInfoEdit.$el.find('.save-button').click();
|
||||
};
|
||||
|
||||
this.cancelNewCourseInfo = function(useCancelButton) {
|
||||
this.courseInfoEdit.onNew(this.event);
|
||||
spyOn(this.courseInfoEdit.$modalCover, 'hide').and.callThrough();
|
||||
|
||||
spyOn(this.courseInfoEdit.$codeMirror, 'getValue').and.returnValue('unsaved changes');
|
||||
const model = this.collection.at(0);
|
||||
spyOn(model, "save").and.callThrough();
|
||||
|
||||
cancelEditingUpdate(this.courseInfoEdit, this.courseInfoEdit.$modalCover, useCancelButton);
|
||||
|
||||
expect(this.courseInfoEdit.$modalCover.hide).toHaveBeenCalled();
|
||||
expect(model.save).not.toHaveBeenCalled();
|
||||
const previewContents = this.courseInfoEdit.$el.find('.update-contents').html();
|
||||
expect(previewContents).not.toEqual('unsaved changes');
|
||||
};
|
||||
|
||||
this.doNotCloseNewCourseInfo = function() {
|
||||
this.courseInfoEdit.onNew(this.event);
|
||||
spyOn(this.courseInfoEdit.$modalCover, 'hide').and.callThrough();
|
||||
|
||||
spyOn(this.courseInfoEdit.$codeMirror, 'getValue').and.returnValue('unsaved changes');
|
||||
const model = this.collection.at(0);
|
||||
spyOn(model, "save").and.callThrough();
|
||||
|
||||
cancelEditingUpdate(this.courseInfoEdit, this.courseInfoEdit.$modalCover, false);
|
||||
|
||||
expect(model.save).not.toHaveBeenCalled();
|
||||
expect(this.courseInfoEdit.$modalCover.hide).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
this.cancelExistingCourseInfo = function(useCancelButton) {
|
||||
this.createNewUpdate('existing update');
|
||||
this.courseInfoEdit.$el.find('.edit-button').click();
|
||||
spyOn(this.courseInfoEdit.$modalCover, 'hide').and.callThrough();
|
||||
|
||||
spyOn(this.courseInfoEdit.$codeMirror, 'getValue').and.returnValue('modification');
|
||||
const model = this.collection.at(0);
|
||||
spyOn(model, "save").and.callThrough();
|
||||
model.id = "saved_to_server";
|
||||
cancelEditingUpdate(this.courseInfoEdit, this.courseInfoEdit.$modalCover, useCancelButton);
|
||||
|
||||
expect(this.courseInfoEdit.$modalCover.hide).toHaveBeenCalled();
|
||||
expect(model.save).not.toHaveBeenCalled();
|
||||
const previewContents = this.courseInfoEdit.$el.find('.update-contents').html();
|
||||
expect(previewContents).toEqual('existing update');
|
||||
};
|
||||
|
||||
this.testInvalidDateValue = function(value) {
|
||||
this.courseInfoEdit.onNew(this.event);
|
||||
expect(this.courseInfoEdit.$el.find('.save-button').hasClass("is-disabled")).toEqual(false);
|
||||
this.courseInfoEdit.$el.find('input.date').val(value).trigger("change");
|
||||
expect(this.courseInfoEdit.$el.find('.save-button').hasClass("is-disabled")).toEqual(true);
|
||||
this.courseInfoEdit.$el.find('input.date').val("01/01/16").trigger("change");
|
||||
expect(this.courseInfoEdit.$el.find('.save-button').hasClass("is-disabled")).toEqual(false);
|
||||
};
|
||||
|
||||
return cancelEditingUpdate = function(update, modalCover, useCancelButton) {
|
||||
if (useCancelButton) {
|
||||
return update.$el.find('.cancel-button').click();
|
||||
} else {
|
||||
return modalCover.click();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it("does send expected data on save", function() {
|
||||
const requests = AjaxHelpers["requests"](this);
|
||||
|
||||
// Create a new update, verifying that the model is created
|
||||
// in the collection and save is called.
|
||||
expect(this.collection.isEmpty()).toBeTruthy();
|
||||
this.courseInfoEdit.onNew(this.event);
|
||||
expect(this.collection.length).toEqual(1);
|
||||
const model = this.collection.at(0);
|
||||
spyOn(model, "save").and.callThrough();
|
||||
spyOn(this.courseInfoEdit.$codeMirror, 'getValue').and.returnValue('/static/image.jpg');
|
||||
|
||||
// Click the "Save button."
|
||||
this.courseInfoEdit.$el.find('.save-button').click();
|
||||
expect(model.save).toHaveBeenCalled();
|
||||
|
||||
// Verify push_notification_selected is set to false.
|
||||
const requestSent = JSON.parse(requests[requests.length - 1].requestBody);
|
||||
expect(requestSent.push_notification_selected).toEqual(false);
|
||||
|
||||
// Verify the link is not rewritten when saved.
|
||||
expect(requestSent.content).toEqual('/static/image.jpg');
|
||||
|
||||
// Verify that analytics are sent
|
||||
expect(window.analytics.track).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does rewrite links for preview", function() {
|
||||
// Create a new update.
|
||||
this.createNewUpdate('/static/image.jpg');
|
||||
|
||||
// Verify the link is rewritten for preview purposes.
|
||||
const previewContents = this.courseInfoEdit.$el.find('.update-contents').html();
|
||||
expect(previewContents).toEqual('base-asset-url/image.jpg');
|
||||
});
|
||||
|
||||
it("shows static links in edit mode", function() {
|
||||
this.createNewUpdate('/static/image.jpg');
|
||||
|
||||
// Click edit and verify CodeMirror contents.
|
||||
this.courseInfoEdit.$el.find('.edit-button').click();
|
||||
expect(this.courseInfoEdit.$codeMirror.getValue()).toEqual('/static/image.jpg');
|
||||
});
|
||||
|
||||
it("removes newly created course info on cancel", function() {
|
||||
this.cancelNewCourseInfo(true);
|
||||
});
|
||||
|
||||
it("do not close new course info on click outside modal", function() {
|
||||
this.doNotCloseNewCourseInfo();
|
||||
});
|
||||
|
||||
it("does not remove existing course info on cancel", function() {
|
||||
this.cancelExistingCourseInfo(true);
|
||||
});
|
||||
|
||||
it("does not remove existing course info on click outside modal", function() {
|
||||
this.cancelExistingCourseInfo(false);
|
||||
});
|
||||
|
||||
it("does not allow updates to be saved with an invalid date", function() {
|
||||
this.testInvalidDateValue("Marchtober 40, 2048");
|
||||
});
|
||||
|
||||
it("does not allow updates to be saved with a blank date", function() {
|
||||
this.testInvalidDateValue("");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("Course Updates WITH Push notification", function() {
|
||||
const courseInfoTemplate = readFixtures('course_info_update.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures($("<script>", {id: "course_info_update-tpl", type: "text/template"}).text(courseInfoTemplate));
|
||||
appendSetFixtures(courseInfoPage);
|
||||
this.collection = new CourseUpdateCollection();
|
||||
this.collection.url = 'course_info_update/';
|
||||
this.courseInfoEdit = new CourseInfoUpdateView({
|
||||
el: $('.course-updates'),
|
||||
collection: this.collection,
|
||||
base_asset_url : 'base-asset-url/',
|
||||
push_notification_enabled : true
|
||||
});
|
||||
this.courseInfoEdit.render();
|
||||
this.event = {preventDefault() { return 'no op'; }};
|
||||
this.courseInfoEdit.onNew(this.event);
|
||||
});
|
||||
|
||||
it("shows push notification checkbox as selected by default", function() {
|
||||
expect(this.courseInfoEdit.$el.find('.toggle-checkbox')).toBeChecked();
|
||||
});
|
||||
|
||||
it("sends correct default value for push_notification_selected", function() {
|
||||
const requests = AjaxHelpers.requests(this);
|
||||
this.courseInfoEdit.$el.find('.save-button').click();
|
||||
const requestSent = JSON.parse(requests[requests.length - 1].requestBody);
|
||||
expect(requestSent.push_notification_selected).toEqual(true);
|
||||
|
||||
// Check that analytics send push_notification info
|
||||
const analytics_payload = window.analytics.track.calls.first().args[1];
|
||||
expect(analytics_payload).toEqual(jasmine.objectContaining({'push_notification_selected': true}));
|
||||
});
|
||||
|
||||
it("sends correct value for push_notification_selected when it is unselected", function() {
|
||||
const requests = AjaxHelpers.requests(this);
|
||||
// unselect push notification
|
||||
this.courseInfoEdit.$el.find('.toggle-checkbox').attr('checked', false);
|
||||
this.courseInfoEdit.$el.find('.save-button').click();
|
||||
const requestSent = JSON.parse(requests[requests.length - 1].requestBody);
|
||||
expect(requestSent.push_notification_selected).toEqual(false);
|
||||
|
||||
// Check that analytics send push_notification info
|
||||
const analytics_payload = window.analytics.track.calls.first().args[1];
|
||||
expect(analytics_payload).toEqual(jasmine.objectContaining({'push_notification_selected': false}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Course Handouts", function() {
|
||||
const handoutsTemplate = readFixtures('course_info_handouts.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures($("<script>", {id: "course_info_handouts-tpl", type: "text/template"}).text(handoutsTemplate));
|
||||
appendSetFixtures(courseInfoPage);
|
||||
|
||||
this.model = new ModuleInfo({
|
||||
id: 'handouts-id',
|
||||
data: '/static/fromServer.jpg'
|
||||
});
|
||||
|
||||
this.handoutsEdit = new CourseInfoHandoutsView({
|
||||
el: $('#course-handouts-view'),
|
||||
model: this.model,
|
||||
base_asset_url: 'base-asset-url/'
|
||||
});
|
||||
|
||||
this.handoutsEdit.render();
|
||||
});
|
||||
|
||||
it("saves <ol></ol> when content left empty", function() {
|
||||
const requests = AjaxHelpers["requests"](this);
|
||||
|
||||
// Enter empty string in the handouts section, verifying that the model
|
||||
// is saved with '<ol></ol>' instead of the empty string
|
||||
this.handoutsEdit.$el.find('.edit-button').click();
|
||||
spyOn(this.handoutsEdit.$codeMirror, 'getValue').and.returnValue('');
|
||||
spyOn(this.model, "save").and.callThrough();
|
||||
this.handoutsEdit.$el.find('.save-button').click();
|
||||
expect(this.model.save).toHaveBeenCalled();
|
||||
|
||||
const contentSaved = JSON.parse(requests[requests.length - 1].requestBody).data;
|
||||
expect(contentSaved).toEqual('<ol></ol>');
|
||||
});
|
||||
|
||||
it("does not rewrite links on save", function() {
|
||||
const requests = AjaxHelpers["requests"](this);
|
||||
|
||||
// Enter something in the handouts section, verifying that the model is saved
|
||||
// when "Save" is clicked.
|
||||
this.handoutsEdit.$el.find('.edit-button').click();
|
||||
spyOn(this.handoutsEdit.$codeMirror, 'getValue').and.returnValue('/static/image.jpg');
|
||||
spyOn(this.model, "save").and.callThrough();
|
||||
this.handoutsEdit.$el.find('.save-button').click();
|
||||
expect(this.model.save).toHaveBeenCalled();
|
||||
|
||||
const contentSaved = JSON.parse(requests[requests.length - 1].requestBody).data;
|
||||
expect(contentSaved).toEqual('/static/image.jpg');
|
||||
});
|
||||
|
||||
it("does rewrite links in initial content", function() {
|
||||
expect(this.handoutsEdit.$preview.html().trim()).toBe('base-asset-url/fromServer.jpg');
|
||||
});
|
||||
|
||||
it("does rewrite links after edit", function() {
|
||||
// Edit handouts and save.
|
||||
this.handoutsEdit.$el.find('.edit-button').click();
|
||||
spyOn(this.handoutsEdit.$codeMirror, 'getValue').and.returnValue('/static/image.jpg');
|
||||
this.handoutsEdit.$el.find('.save-button').click();
|
||||
|
||||
// Verify preview text.
|
||||
expect(this.handoutsEdit.$preview.html().trim()).toBe('base-asset-url/image.jpg');
|
||||
});
|
||||
|
||||
it("shows static links in edit mode", function() {
|
||||
// Click edit and verify CodeMirror contents.
|
||||
this.handoutsEdit.$el.find('.edit-button').click();
|
||||
expect(this.handoutsEdit.$codeMirror.getValue().trim()).toEqual('/static/fromServer.jpg');
|
||||
});
|
||||
|
||||
it("can open course handouts with bad html on edit", function() {
|
||||
// Enter some bad html in handouts section, verifying that the
|
||||
// model/handoutform opens when "Edit" is clicked
|
||||
|
||||
this.model = new ModuleInfo({
|
||||
id: 'handouts-id',
|
||||
data: '<p><a href="[URL OF FILE]>[LINK TEXT]</a></p>'
|
||||
});
|
||||
this.handoutsEdit = new CourseInfoHandoutsView({
|
||||
el: $('#course-handouts-view'),
|
||||
model: this.model,
|
||||
base_asset_url: 'base-asset-url/'
|
||||
});
|
||||
this.handoutsEdit.render();
|
||||
|
||||
expect($('.edit-handouts-form').is(':hidden')).toEqual(true);
|
||||
this.handoutsEdit.$el.find('.edit-button').click();
|
||||
expect(this.handoutsEdit.$codeMirror.getValue()).toEqual('<p><a href="[URL OF FILE]>[LINK TEXT]</a></p>');
|
||||
expect($('.edit-handouts-form').is(':hidden')).toEqual(false);
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
@@ -1,608 +0,0 @@
|
||||
define ["js/models/metadata", "js/collections/metadata", "js/views/metadata", "cms/js/main"],
|
||||
(MetadataModel, MetadataCollection, MetadataView, main) ->
|
||||
verifyInputType = (input, expectedType) ->
|
||||
# Some browsers (e.g. FireFox) do not support the "number"
|
||||
# input type. We can accept a "text" input instead
|
||||
# and still get acceptable behavior in the UI.
|
||||
if expectedType == 'number' and input.type != 'number'
|
||||
expectedType = 'text'
|
||||
expect(input.type).toBe(expectedType)
|
||||
|
||||
describe "Test Metadata Editor", ->
|
||||
editorTemplate = readFixtures('metadata-editor.underscore')
|
||||
numberEntryTemplate = readFixtures('metadata-number-entry.underscore')
|
||||
stringEntryTemplate = readFixtures('metadata-string-entry.underscore')
|
||||
optionEntryTemplate = readFixtures('metadata-option-entry.underscore')
|
||||
listEntryTemplate = readFixtures('metadata-list-entry.underscore')
|
||||
dictEntryTemplate = readFixtures('metadata-dict-entry.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "metadata-editor-tpl", type: "text/template"}).text(editorTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-number-entry", type: "text/template"}).text(numberEntryTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-string-entry", type: "text/template"}).text(stringEntryTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-option-entry", type: "text/template"}).text(optionEntryTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-list-entry", type: "text/template"}).text(listEntryTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-dict-entry", type: "text/template"}).text(dictEntryTemplate))
|
||||
|
||||
genericEntry = {
|
||||
default_value: 'default value',
|
||||
display_name: "Display Name",
|
||||
explicitly_set: true,
|
||||
field_name: "display_name",
|
||||
help: "Specifies the name for this component.",
|
||||
options: [],
|
||||
type: MetadataModel.GENERIC_TYPE,
|
||||
value: "Word cloud"
|
||||
}
|
||||
|
||||
selectEntry = {
|
||||
default_value: "answered",
|
||||
display_name: "Show Answer",
|
||||
explicitly_set: false,
|
||||
field_name: "show_answer",
|
||||
help: "When should you show the answer",
|
||||
options: [
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "Answered", "value": "answered"},
|
||||
{"display_name": "Never", "value": "never"}
|
||||
],
|
||||
type: MetadataModel.SELECT_TYPE,
|
||||
value: "always"
|
||||
}
|
||||
|
||||
integerEntry = {
|
||||
default_value: 6,
|
||||
display_name: "Inputs",
|
||||
explicitly_set: false,
|
||||
field_name: "num_inputs",
|
||||
help: "Number of text boxes for student to input words/sentences.",
|
||||
options: {min: 1},
|
||||
type: MetadataModel.INTEGER_TYPE,
|
||||
value: 5
|
||||
}
|
||||
|
||||
floatEntry = {
|
||||
default_value: 2.7,
|
||||
display_name: "Weight",
|
||||
explicitly_set: true,
|
||||
field_name: "weight",
|
||||
help: "Weight for this problem",
|
||||
options: {min: 1.3, max:100.2, step:0.1},
|
||||
type: MetadataModel.FLOAT_TYPE,
|
||||
value: 10.2
|
||||
}
|
||||
|
||||
listEntry = {
|
||||
default_value: ["a thing", "another thing"],
|
||||
display_name: "List",
|
||||
explicitly_set: false,
|
||||
field_name: "list",
|
||||
help: "A list of things.",
|
||||
options: [],
|
||||
type: MetadataModel.LIST_TYPE,
|
||||
value: ["the first display value", "the second"]
|
||||
}
|
||||
|
||||
timeEntry = {
|
||||
default_value: "00:00:00",
|
||||
display_name: "Time",
|
||||
explicitly_set: true,
|
||||
field_name: "relative_time",
|
||||
help: "Specifies the name for this component.",
|
||||
options: [],
|
||||
type: MetadataModel.RELATIVE_TIME_TYPE,
|
||||
value: "12:12:12"
|
||||
}
|
||||
|
||||
dictEntry = {
|
||||
default_value: {
|
||||
'en': 'English',
|
||||
'ru': 'Русский'
|
||||
},
|
||||
display_name: "New Dict",
|
||||
explicitly_set: false,
|
||||
field_name: "dict",
|
||||
help: "Specifies the name for this component.",
|
||||
type: MetadataModel.DICT_TYPE,
|
||||
value: {
|
||||
'en': 'English',
|
||||
'ru': 'Русский',
|
||||
'ua': 'Українська',
|
||||
'fr': 'Français'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Test for the editor that creates the individual views.
|
||||
describe "MetadataView.Editor creates editors for each field", ->
|
||||
beforeEach ->
|
||||
@model = new MetadataCollection(
|
||||
[
|
||||
integerEntry,
|
||||
floatEntry,
|
||||
selectEntry,
|
||||
genericEntry,
|
||||
{
|
||||
default_value: null,
|
||||
display_name: "Unknown",
|
||||
explicitly_set: true,
|
||||
field_name: "unknown_type",
|
||||
help: "Mystery property.",
|
||||
options: [
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "Answered", "value": "answered"},
|
||||
{"display_name": "Never", "value": "never"}],
|
||||
type: "unknown type",
|
||||
value: null
|
||||
},
|
||||
listEntry,
|
||||
timeEntry,
|
||||
dictEntry
|
||||
]
|
||||
)
|
||||
|
||||
it "creates child views on initialize, and sorts them alphabetically", ->
|
||||
view = new MetadataView.Editor({collection: @model})
|
||||
childModels = view.collection.models
|
||||
expect(childModels.length).toBe(8)
|
||||
# Be sure to check list view as well as other input types
|
||||
childViews = view.$el.find('.setting-input, .list-settings')
|
||||
expect(childViews.length).toBe(8)
|
||||
|
||||
verifyEntry = (index, display_name, type) ->
|
||||
expect(childModels[index].get('display_name')).toBe(display_name)
|
||||
verifyInputType(childViews[index], type)
|
||||
|
||||
verifyEntry(0, 'Display Name', 'text')
|
||||
verifyEntry(1, 'Inputs', 'number')
|
||||
verifyEntry(2, 'List', '')
|
||||
verifyEntry(3, 'New Dict', '')
|
||||
verifyEntry(4, 'Show Answer', 'select-one')
|
||||
verifyEntry(5, 'Time', 'text')
|
||||
verifyEntry(6, 'Unknown', 'text')
|
||||
verifyEntry(7, 'Weight', 'number')
|
||||
|
||||
it "returns its display name", ->
|
||||
view = new MetadataView.Editor({collection: @model})
|
||||
expect(view.getDisplayName()).toBe("Word cloud")
|
||||
|
||||
it "returns an empty string if there is no display name property with a valid value", ->
|
||||
view = new MetadataView.Editor({collection: new MetadataCollection()})
|
||||
expect(view.getDisplayName()).toBe("")
|
||||
|
||||
view = new MetadataView.Editor({collection: new MetadataCollection([
|
||||
{
|
||||
default_value: null,
|
||||
display_name: "Display Name",
|
||||
explicitly_set: false,
|
||||
field_name: "display_name",
|
||||
help: "",
|
||||
options: [],
|
||||
type: MetadataModel.GENERIC_TYPE,
|
||||
value: null
|
||||
|
||||
}])
|
||||
})
|
||||
expect(view.getDisplayName()).toBe("")
|
||||
|
||||
it "has no modified values by default", ->
|
||||
view = new MetadataView.Editor({collection: @model})
|
||||
expect(view.getModifiedMetadataValues()).toEqual({})
|
||||
|
||||
it "returns modified values only", ->
|
||||
view = new MetadataView.Editor({collection: @model})
|
||||
childModels = view.collection.models
|
||||
childModels[0].setValue('updated display name')
|
||||
childModels[1].setValue(20)
|
||||
expect(view.getModifiedMetadataValues()).toEqual({
|
||||
display_name : 'updated display name',
|
||||
num_inputs: 20
|
||||
})
|
||||
|
||||
# Tests for individual views.
|
||||
assertInputType = (view, expectedType) ->
|
||||
input = view.$el.find('.setting-input')
|
||||
expect(input.length).toEqual(1)
|
||||
verifyInputType(input[0], expectedType)
|
||||
|
||||
assertValueInView = (view, expectedValue) ->
|
||||
expect(view.getValueFromEditor()).toEqual(expectedValue)
|
||||
|
||||
assertCanUpdateView = (view, newValue) ->
|
||||
view.setValueInEditor(newValue)
|
||||
expect(view.getValueFromEditor()).toEqual(newValue)
|
||||
|
||||
assertClear = (view, modelValue, editorValue=modelValue) ->
|
||||
view.clear()
|
||||
expect(view.model.getValue()).toBe(null)
|
||||
expect(view.model.getDisplayValue()).toEqual(modelValue)
|
||||
expect(view.getValueFromEditor()).toEqual(editorValue)
|
||||
|
||||
assertUpdateModel = (view, originalValue, newValue) ->
|
||||
view.setValueInEditor(newValue)
|
||||
expect(view.model.getValue()).toEqual(originalValue)
|
||||
view.updateModel()
|
||||
expect(view.model.getValue()).toEqual(newValue)
|
||||
|
||||
describe "MetadataView.String is a basic string input with clear functionality", ->
|
||||
beforeEach ->
|
||||
model = new MetadataModel(genericEntry)
|
||||
@view = new MetadataView.String({model: model})
|
||||
|
||||
it "uses a text input type", ->
|
||||
assertInputType(@view, 'text')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@view, 'Word cloud')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@view, "updated ' \" &")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@view, 'default value')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@view, 'Word cloud', 'updated')
|
||||
|
||||
describe "MetadataView.Option is an option input type with clear functionality", ->
|
||||
beforeEach ->
|
||||
model = new MetadataModel(selectEntry)
|
||||
@view = new MetadataView.Option({model: model})
|
||||
|
||||
it "uses a select input type", ->
|
||||
assertInputType(@view, 'select-one')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@view, 'always')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@view, "never")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@view, 'answered')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@view, null, 'never')
|
||||
|
||||
it "does not update to a value that is not an option", ->
|
||||
@view.setValueInEditor("not an option")
|
||||
expect(@view.getValueFromEditor()).toBe('always')
|
||||
|
||||
describe "MetadataView.Number supports integer or float type and has clear functionality", ->
|
||||
verifyValueAfterChanged = (view, value, expectedResult) ->
|
||||
view.setValueInEditor(value)
|
||||
view.changed()
|
||||
expect(view.getValueFromEditor()).toBe(expectedResult)
|
||||
|
||||
beforeEach ->
|
||||
integerModel = new MetadataModel(integerEntry)
|
||||
@integerView = new MetadataView.Number({model: integerModel})
|
||||
|
||||
floatModel = new MetadataModel(floatEntry)
|
||||
@floatView = new MetadataView.Number({model: floatModel})
|
||||
|
||||
it "uses a number input type", ->
|
||||
assertInputType(@integerView, 'number')
|
||||
assertInputType(@floatView, 'number')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@integerView, '5')
|
||||
assertValueInView(@floatView, '10.2')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@integerView, "12")
|
||||
assertCanUpdateView(@floatView, "-2.4")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@integerView, 6, '6')
|
||||
assertClear(@floatView, 2.7, '2.7')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@integerView, null, '90')
|
||||
assertUpdateModel(@floatView, 10.2, '-9.5')
|
||||
|
||||
it "knows the difference between integer and float", ->
|
||||
expect(@integerView.isIntegerField()).toBeTruthy()
|
||||
expect(@floatView.isIntegerField()).toBeFalsy()
|
||||
|
||||
it "sets attribtues related to min, max, and step", ->
|
||||
verifyAttributes = (view, min, step, max=null) ->
|
||||
inputEntry = view.$el.find('input')
|
||||
expect(Number(inputEntry.attr('min'))).toEqual(min)
|
||||
expect(Number(inputEntry.attr('step'))).toEqual(step)
|
||||
if max is not null
|
||||
expect(Number(inputEntry.attr('max'))).toEqual(max)
|
||||
|
||||
verifyAttributes(@integerView, 1, 1)
|
||||
verifyAttributes(@floatView, 1.3, .1, 100.2)
|
||||
|
||||
it "corrects values that are out of range", ->
|
||||
verifyValueAfterChanged(@integerView, '-4', '1')
|
||||
verifyValueAfterChanged(@integerView, '1', '1')
|
||||
verifyValueAfterChanged(@integerView, '0', '1')
|
||||
verifyValueAfterChanged(@integerView, '3001', '3001')
|
||||
|
||||
verifyValueAfterChanged(@floatView, '-4', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '1.3', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '1.2', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '100.2', '100.2')
|
||||
verifyValueAfterChanged(@floatView, '100.3', '100.2')
|
||||
|
||||
it "sets default values for integer and float fields that are empty", ->
|
||||
verifyValueAfterChanged(@integerView, '', '6')
|
||||
verifyValueAfterChanged(@floatView, '', '2.7')
|
||||
|
||||
it "disallows invalid characters", ->
|
||||
verifyValueAfterKeyPressed = (view, character, reject) ->
|
||||
event = {
|
||||
type : 'keypress',
|
||||
which : character.charCodeAt(0),
|
||||
keyCode: character.charCodeAt(0),
|
||||
preventDefault : () -> 'no op'
|
||||
}
|
||||
spyOn(event, 'preventDefault')
|
||||
view.$el.find('input').trigger(event)
|
||||
if (reject)
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
else
|
||||
expect(event.preventDefault).not.toHaveBeenCalled()
|
||||
|
||||
verifyDisallowedChars = (view) ->
|
||||
verifyValueAfterKeyPressed(view, 'a', true)
|
||||
verifyValueAfterKeyPressed(view, '.', view.isIntegerField())
|
||||
verifyValueAfterKeyPressed(view, '[', true)
|
||||
verifyValueAfterKeyPressed(view, '@', true)
|
||||
|
||||
for i in [0...9]
|
||||
verifyValueAfterKeyPressed(view, String(i), false)
|
||||
|
||||
verifyDisallowedChars(@integerView)
|
||||
verifyDisallowedChars(@floatView)
|
||||
|
||||
describe "MetadataView.List allows the user to enter an ordered list of strings", ->
|
||||
beforeEach ->
|
||||
listModel = new MetadataModel(listEntry)
|
||||
@listView = new MetadataView.List({model: listModel})
|
||||
@el = @listView.$el
|
||||
main()
|
||||
|
||||
it "returns the initial value upon initialization", ->
|
||||
assertValueInView(@listView, ['the first display value', 'the second'])
|
||||
|
||||
it "updates its value correctly", ->
|
||||
assertCanUpdateView(@listView, ['a new item', 'another new item', 'a third'])
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
@el.find('.create-setting').click()
|
||||
assertClear(@listView, ['a thing', 'another thing'])
|
||||
expect(@el.find('.create-setting')).not.toHaveClass('is-disabled')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@listView, null, ['a new value'])
|
||||
|
||||
it "can add an entry", ->
|
||||
expect(@listView.model.get('value').length).toEqual(2)
|
||||
@el.find('.create-setting').click()
|
||||
expect(@el.find('input.input').length).toEqual(3)
|
||||
|
||||
it "can remove an entry", ->
|
||||
expect(@listView.model.get('value').length).toEqual(2)
|
||||
@el.find('.remove-setting').first().click()
|
||||
expect(@listView.model.get('value').length).toEqual(1)
|
||||
|
||||
it "only allows one blank entry at a time", ->
|
||||
expect(@el.find('input').length).toEqual(2)
|
||||
@el.find('.create-setting').click()
|
||||
@el.find('.create-setting').click()
|
||||
expect(@el.find('input').length).toEqual(3)
|
||||
|
||||
it "re-enables the add setting button after entering a new value", ->
|
||||
expect(@el.find('input').length).toEqual(2)
|
||||
@el.find('.create-setting').click()
|
||||
expect(@el.find('.create-setting')).toHaveClass('is-disabled')
|
||||
@el.find('input').last().val('third setting')
|
||||
@el.find('input').last().trigger('input')
|
||||
expect(@el.find('.create-setting')).not.toHaveClass('is-disabled')
|
||||
|
||||
describe "MetadataView.RelativeTime allows the user to enter time string in HH:mm:ss format", ->
|
||||
beforeEach ->
|
||||
model = new MetadataModel(timeEntry)
|
||||
@view = new MetadataView.RelativeTime({model: model})
|
||||
|
||||
it "uses a text input type", ->
|
||||
assertInputType(@view, 'text')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@view, '12:12:12')
|
||||
|
||||
it "value is converted correctly", ->
|
||||
view = @view
|
||||
|
||||
cases = [
|
||||
{
|
||||
input: '23:100:0'
|
||||
output: '23:59:59'
|
||||
},
|
||||
{
|
||||
input: '100000000000000000'
|
||||
output: '23:59:59'
|
||||
},
|
||||
{
|
||||
input: '80000'
|
||||
output: '22:13:20'
|
||||
},
|
||||
{
|
||||
input: '-100'
|
||||
output: '00:00:00'
|
||||
},
|
||||
{
|
||||
input: '-100:-10'
|
||||
output: '00:00:00'
|
||||
},
|
||||
{
|
||||
input: '99:99'
|
||||
output: '01:40:39'
|
||||
},
|
||||
{
|
||||
input: '2'
|
||||
output: '00:00:02'
|
||||
},
|
||||
{
|
||||
input: '1:2'
|
||||
output: '00:01:02'
|
||||
},
|
||||
{
|
||||
input: '1:25'
|
||||
output: '00:01:25'
|
||||
},
|
||||
{
|
||||
input: '3:1:25'
|
||||
output: '03:01:25'
|
||||
},
|
||||
{
|
||||
input: ' 2 3 : 5 9 : 5 9 '
|
||||
output: '23:59:59'
|
||||
},
|
||||
{
|
||||
input: '9:1:25'
|
||||
output: '09:01:25'
|
||||
},
|
||||
{
|
||||
input: '77:72:77'
|
||||
output: '23:59:59'
|
||||
},
|
||||
{
|
||||
input: '22:100:100'
|
||||
output: '23:41:40'
|
||||
},
|
||||
# negative value
|
||||
{
|
||||
input: '-22:22:22'
|
||||
output: '00:22:22'
|
||||
},
|
||||
# simple string
|
||||
{
|
||||
input: 'simple text'
|
||||
output: '00:00:00'
|
||||
},
|
||||
{
|
||||
input: 'a10a:a10a:a10a'
|
||||
output: '00:00:00'
|
||||
},
|
||||
# empty string
|
||||
{
|
||||
input: ''
|
||||
output: '00:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
$.each cases, (index, data) ->
|
||||
expect(view.parseRelativeTime(data.input)).toBe(data.output)
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@view, "23:59:59")
|
||||
@view.setValueInEditor("33:59:59")
|
||||
@view.updateModel()
|
||||
assertValueInView(@view, "23:59:59")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@view, '00:00:00')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@view, '12:12:12', '23:59:59')
|
||||
|
||||
describe "MetadataView.Dict allows the user to enter key-value pairs of strings", ->
|
||||
beforeEach ->
|
||||
dictModel = new MetadataModel($.extend(true, {}, dictEntry))
|
||||
@dictView = new MetadataView.Dict({model: dictModel})
|
||||
@el = @dictView.$el
|
||||
main()
|
||||
|
||||
it "returns the initial value upon initialization", ->
|
||||
assertValueInView(@dictView, {
|
||||
'en': 'English',
|
||||
'ru': 'Русский',
|
||||
'ua': 'Українська',
|
||||
'fr': 'Français'
|
||||
})
|
||||
|
||||
it "updates its value correctly", ->
|
||||
assertCanUpdateView(@dictView, {
|
||||
'ru': 'Русский',
|
||||
'ua': 'Українська',
|
||||
'fr': 'Français'
|
||||
})
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
@el.find('.create-setting').click()
|
||||
assertClear(@dictView, {
|
||||
'en': 'English',
|
||||
'ru': 'Русский'
|
||||
})
|
||||
expect(@el.find('.create-setting')).not.toHaveClass('is-disabled')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@dictView, null, {'fr': 'Français'})
|
||||
|
||||
it "can add an entry", ->
|
||||
expect(_.keys(@dictView.model.get('value')).length).toEqual(4)
|
||||
@el.find('.create-setting').click()
|
||||
expect(@el.find('input.input-key').length).toEqual(5)
|
||||
|
||||
it "can remove an entry", ->
|
||||
expect(_.keys(@dictView.model.get('value')).length).toEqual(4)
|
||||
@el.find('.remove-setting').first().click()
|
||||
expect(_.keys(@dictView.model.get('value')).length).toEqual(3)
|
||||
|
||||
it "only allows one blank entry at a time", ->
|
||||
expect(@el.find('input.input-key').length).toEqual(4)
|
||||
@el.find('.create-setting').click()
|
||||
@el.find('.create-setting').click()
|
||||
expect(@el.find('input.input-key').length).toEqual(5)
|
||||
|
||||
it "only allows unique keys", ->
|
||||
data = [
|
||||
{
|
||||
expectedValue: {'ru': 'Русский'},
|
||||
initialValue: {'ru': 'Русский'},
|
||||
testValue: {
|
||||
'key': 'ru'
|
||||
'value': ''
|
||||
}
|
||||
},
|
||||
{
|
||||
expectedValue: {'ru': 'Русский'},
|
||||
initialValue: {'ru': 'Some value'},
|
||||
testValue: {
|
||||
'key': 'ru'
|
||||
'value': 'Русский'
|
||||
}
|
||||
},
|
||||
{
|
||||
expectedValue: {'ru': 'Русский'},
|
||||
initialValue: {'ru': 'Русский'},
|
||||
testValue: {
|
||||
'key': ''
|
||||
'value': ''
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
_.each data, ((d, index) ->
|
||||
@dictView.setValueInEditor(d.initialValue)
|
||||
@dictView.updateModel();
|
||||
@el.find('.create-setting').click()
|
||||
item = @el.find('.list-settings-item').last()
|
||||
item.find('.input-key').val(d.testValue.key);
|
||||
item.find('.input-value').val(d.testValue.value);
|
||||
|
||||
expect(@dictView.getValueFromEditor()).toEqual(d.expectedValue)
|
||||
).bind(@)
|
||||
|
||||
it "re-enables the add setting button after entering a new value", ->
|
||||
expect(@el.find('input.input-key').length).toEqual(4)
|
||||
@el.find('.create-setting').click()
|
||||
expect(@el.find('.create-setting')).toHaveClass('is-disabled')
|
||||
@el.find('input.input-key').last().val('third setting')
|
||||
@el.find('input.input-key').last().trigger('input')
|
||||
expect(@el.find('.create-setting')).not.toHaveClass('is-disabled')
|
||||
692
cms/static/coffee/spec/views/metadata_edit_spec.js
Normal file
692
cms/static/coffee/spec/views/metadata_edit_spec.js
Normal file
@@ -0,0 +1,692 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
define(["js/models/metadata", "js/collections/metadata", "js/views/metadata", "cms/js/main"],
|
||||
function(MetadataModel, MetadataCollection, MetadataView, main) {
|
||||
const verifyInputType = function(input, expectedType) {
|
||||
// Some browsers (e.g. FireFox) do not support the "number"
|
||||
// input type. We can accept a "text" input instead
|
||||
// and still get acceptable behavior in the UI.
|
||||
if ((expectedType === 'number') && (input.type !== 'number')) {
|
||||
expectedType = 'text';
|
||||
}
|
||||
expect(input.type).toBe(expectedType);
|
||||
};
|
||||
|
||||
describe("Test Metadata Editor", function() {
|
||||
const editorTemplate = readFixtures('metadata-editor.underscore');
|
||||
const numberEntryTemplate = readFixtures('metadata-number-entry.underscore');
|
||||
const stringEntryTemplate = readFixtures('metadata-string-entry.underscore');
|
||||
const optionEntryTemplate = readFixtures('metadata-option-entry.underscore');
|
||||
const listEntryTemplate = readFixtures('metadata-list-entry.underscore');
|
||||
const dictEntryTemplate = readFixtures('metadata-dict-entry.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures($("<script>", {id: "metadata-editor-tpl", type: "text/template"}).text(editorTemplate));
|
||||
appendSetFixtures($("<script>", {id: "metadata-number-entry", type: "text/template"}).text(numberEntryTemplate));
|
||||
appendSetFixtures($("<script>", {id: "metadata-string-entry", type: "text/template"}).text(stringEntryTemplate));
|
||||
appendSetFixtures($("<script>", {id: "metadata-option-entry", type: "text/template"}).text(optionEntryTemplate));
|
||||
appendSetFixtures($("<script>", {id: "metadata-list-entry", type: "text/template"}).text(listEntryTemplate));
|
||||
appendSetFixtures($("<script>", {id: "metadata-dict-entry", type: "text/template"}).text(dictEntryTemplate));
|
||||
});
|
||||
|
||||
const genericEntry = {
|
||||
default_value: 'default value',
|
||||
display_name: "Display Name",
|
||||
explicitly_set: true,
|
||||
field_name: "display_name",
|
||||
help: "Specifies the name for this component.",
|
||||
options: [],
|
||||
type: MetadataModel.GENERIC_TYPE,
|
||||
value: "Word cloud"
|
||||
};
|
||||
|
||||
const selectEntry = {
|
||||
default_value: "answered",
|
||||
display_name: "Show Answer",
|
||||
explicitly_set: false,
|
||||
field_name: "show_answer",
|
||||
help: "When should you show the answer",
|
||||
options: [
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "Answered", "value": "answered"},
|
||||
{"display_name": "Never", "value": "never"}
|
||||
],
|
||||
type: MetadataModel.SELECT_TYPE,
|
||||
value: "always"
|
||||
};
|
||||
|
||||
const integerEntry = {
|
||||
default_value: 6,
|
||||
display_name: "Inputs",
|
||||
explicitly_set: false,
|
||||
field_name: "num_inputs",
|
||||
help: "Number of text boxes for student to input words/sentences.",
|
||||
options: {min: 1},
|
||||
type: MetadataModel.INTEGER_TYPE,
|
||||
value: 5
|
||||
};
|
||||
|
||||
const floatEntry = {
|
||||
default_value: 2.7,
|
||||
display_name: "Weight",
|
||||
explicitly_set: true,
|
||||
field_name: "weight",
|
||||
help: "Weight for this problem",
|
||||
options: {min: 1.3, max:100.2, step:0.1},
|
||||
type: MetadataModel.FLOAT_TYPE,
|
||||
value: 10.2
|
||||
};
|
||||
|
||||
const listEntry = {
|
||||
default_value: ["a thing", "another thing"],
|
||||
display_name: "List",
|
||||
explicitly_set: false,
|
||||
field_name: "list",
|
||||
help: "A list of things.",
|
||||
options: [],
|
||||
type: MetadataModel.LIST_TYPE,
|
||||
value: ["the first display value", "the second"]
|
||||
};
|
||||
|
||||
const timeEntry = {
|
||||
default_value: "00:00:00",
|
||||
display_name: "Time",
|
||||
explicitly_set: true,
|
||||
field_name: "relative_time",
|
||||
help: "Specifies the name for this component.",
|
||||
options: [],
|
||||
type: MetadataModel.RELATIVE_TIME_TYPE,
|
||||
value: "12:12:12"
|
||||
};
|
||||
|
||||
const dictEntry = {
|
||||
default_value: {
|
||||
'en': 'English',
|
||||
'ru': 'Русский'
|
||||
},
|
||||
display_name: "New Dict",
|
||||
explicitly_set: false,
|
||||
field_name: "dict",
|
||||
help: "Specifies the name for this component.",
|
||||
type: MetadataModel.DICT_TYPE,
|
||||
value: {
|
||||
'en': 'English',
|
||||
'ru': 'Русский',
|
||||
'ua': 'Українська',
|
||||
'fr': 'Français'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Test for the editor that creates the individual views.
|
||||
describe("MetadataView.Editor creates editors for each field", function() {
|
||||
beforeEach(function() {
|
||||
this.model = new MetadataCollection(
|
||||
[
|
||||
integerEntry,
|
||||
floatEntry,
|
||||
selectEntry,
|
||||
genericEntry,
|
||||
{
|
||||
default_value: null,
|
||||
display_name: "Unknown",
|
||||
explicitly_set: true,
|
||||
field_name: "unknown_type",
|
||||
help: "Mystery property.",
|
||||
options: [
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "Answered", "value": "answered"},
|
||||
{"display_name": "Never", "value": "never"}],
|
||||
type: "unknown type",
|
||||
value: null
|
||||
},
|
||||
listEntry,
|
||||
timeEntry,
|
||||
dictEntry
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it("creates child views on initialize, and sorts them alphabetically", function() {
|
||||
const view = new MetadataView.Editor({collection: this.model});
|
||||
const childModels = view.collection.models;
|
||||
expect(childModels.length).toBe(8);
|
||||
// Be sure to check list view as well as other input types
|
||||
const childViews = view.$el.find('.setting-input, .list-settings');
|
||||
expect(childViews.length).toBe(8);
|
||||
|
||||
const verifyEntry = function(index, display_name, type) {
|
||||
expect(childModels[index].get('display_name')).toBe(display_name);
|
||||
verifyInputType(childViews[index], type);
|
||||
};
|
||||
|
||||
verifyEntry(0, 'Display Name', 'text');
|
||||
verifyEntry(1, 'Inputs', 'number');
|
||||
verifyEntry(2, 'List', '');
|
||||
verifyEntry(3, 'New Dict', '');
|
||||
verifyEntry(4, 'Show Answer', 'select-one');
|
||||
verifyEntry(5, 'Time', 'text');
|
||||
verifyEntry(6, 'Unknown', 'text');
|
||||
verifyEntry(7, 'Weight', 'number');
|
||||
});
|
||||
|
||||
it("returns its display name", function() {
|
||||
const view = new MetadataView.Editor({collection: this.model});
|
||||
expect(view.getDisplayName()).toBe("Word cloud");
|
||||
});
|
||||
|
||||
it("returns an empty string if there is no display name property with a valid value", function() {
|
||||
let view = new MetadataView.Editor({collection: new MetadataCollection()});
|
||||
expect(view.getDisplayName()).toBe("");
|
||||
|
||||
view = new MetadataView.Editor({collection: new MetadataCollection([
|
||||
{
|
||||
default_value: null,
|
||||
display_name: "Display Name",
|
||||
explicitly_set: false,
|
||||
field_name: "display_name",
|
||||
help: "",
|
||||
options: [],
|
||||
type: MetadataModel.GENERIC_TYPE,
|
||||
value: null
|
||||
|
||||
}])
|
||||
});
|
||||
expect(view.getDisplayName()).toBe("");
|
||||
});
|
||||
|
||||
it("has no modified values by default", function() {
|
||||
const view = new MetadataView.Editor({collection: this.model});
|
||||
expect(view.getModifiedMetadataValues()).toEqual({});
|
||||
});
|
||||
|
||||
it("returns modified values only", function() {
|
||||
const view = new MetadataView.Editor({collection: this.model});
|
||||
const childModels = view.collection.models;
|
||||
childModels[0].setValue('updated display name');
|
||||
childModels[1].setValue(20);
|
||||
expect(view.getModifiedMetadataValues()).toEqual({
|
||||
display_name : 'updated display name',
|
||||
num_inputs: 20
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Tests for individual views.
|
||||
const assertInputType = function(view, expectedType) {
|
||||
const input = view.$el.find('.setting-input');
|
||||
expect(input.length).toEqual(1);
|
||||
verifyInputType(input[0], expectedType);
|
||||
};
|
||||
|
||||
const assertValueInView = (view, expectedValue) => expect(view.getValueFromEditor()).toEqual(expectedValue);
|
||||
|
||||
const assertCanUpdateView = function(view, newValue) {
|
||||
view.setValueInEditor(newValue);
|
||||
expect(view.getValueFromEditor()).toEqual(newValue);
|
||||
};
|
||||
|
||||
const assertClear = function(view, modelValue, editorValue) {
|
||||
if (editorValue == null) { editorValue = modelValue; }
|
||||
view.clear();
|
||||
expect(view.model.getValue()).toBe(null);
|
||||
expect(view.model.getDisplayValue()).toEqual(modelValue);
|
||||
expect(view.getValueFromEditor()).toEqual(editorValue);
|
||||
};
|
||||
|
||||
const assertUpdateModel = function(view, originalValue, newValue) {
|
||||
view.setValueInEditor(newValue);
|
||||
expect(view.model.getValue()).toEqual(originalValue);
|
||||
view.updateModel();
|
||||
expect(view.model.getValue()).toEqual(newValue);
|
||||
};
|
||||
|
||||
describe("MetadataView.String is a basic string input with clear functionality", function() {
|
||||
beforeEach(function() {
|
||||
const model = new MetadataModel(genericEntry);
|
||||
this.view = new MetadataView.String({model});
|
||||
});
|
||||
|
||||
it("uses a text input type", function() {
|
||||
assertInputType(this.view, 'text');
|
||||
});
|
||||
|
||||
it("returns the intial value upon initialization", function() {
|
||||
assertValueInView(this.view, 'Word cloud');
|
||||
});
|
||||
|
||||
it("can update its value in the view", function() {
|
||||
assertCanUpdateView(this.view, "updated ' \" &");
|
||||
});
|
||||
|
||||
it("has a clear method to revert to the model default", function() {
|
||||
assertClear(this.view, 'default value');
|
||||
});
|
||||
|
||||
it("has an update model method", function() {
|
||||
assertUpdateModel(this.view, 'Word cloud', 'updated');
|
||||
});
|
||||
});
|
||||
|
||||
describe("MetadataView.Option is an option input type with clear functionality", function() {
|
||||
beforeEach(function() {
|
||||
const model = new MetadataModel(selectEntry);
|
||||
this.view = new MetadataView.Option({model});
|
||||
});
|
||||
|
||||
it("uses a select input type", function() {
|
||||
assertInputType(this.view, 'select-one');
|
||||
});
|
||||
|
||||
it("returns the intial value upon initialization", function() {
|
||||
assertValueInView(this.view, 'always');
|
||||
});
|
||||
|
||||
it("can update its value in the view", function() {
|
||||
assertCanUpdateView(this.view, "never");
|
||||
});
|
||||
|
||||
it("has a clear method to revert to the model default", function() {
|
||||
assertClear(this.view, 'answered');
|
||||
});
|
||||
|
||||
it("has an update model method", function() {
|
||||
assertUpdateModel(this.view, null, 'never');
|
||||
});
|
||||
|
||||
it("does not update to a value that is not an option", function() {
|
||||
this.view.setValueInEditor("not an option");
|
||||
expect(this.view.getValueFromEditor()).toBe('always');
|
||||
});
|
||||
});
|
||||
|
||||
describe("MetadataView.Number supports integer or float type and has clear functionality", function() {
|
||||
const verifyValueAfterChanged = function(view, value, expectedResult) {
|
||||
view.setValueInEditor(value);
|
||||
view.changed();
|
||||
expect(view.getValueFromEditor()).toBe(expectedResult);
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
const integerModel = new MetadataModel(integerEntry);
|
||||
this.integerView = new MetadataView.Number({model: integerModel});
|
||||
|
||||
const floatModel = new MetadataModel(floatEntry);
|
||||
this.floatView = new MetadataView.Number({model: floatModel});
|
||||
});
|
||||
|
||||
it("uses a number input type", function() {
|
||||
assertInputType(this.integerView, 'number');
|
||||
assertInputType(this.floatView, 'number');
|
||||
});
|
||||
|
||||
it("returns the intial value upon initialization", function() {
|
||||
assertValueInView(this.integerView, '5');
|
||||
assertValueInView(this.floatView, '10.2');
|
||||
});
|
||||
|
||||
it("can update its value in the view", function() {
|
||||
assertCanUpdateView(this.integerView, "12");
|
||||
assertCanUpdateView(this.floatView, "-2.4");
|
||||
});
|
||||
|
||||
it("has a clear method to revert to the model default", function() {
|
||||
assertClear(this.integerView, 6, '6');
|
||||
assertClear(this.floatView, 2.7, '2.7');
|
||||
});
|
||||
|
||||
it("has an update model method", function() {
|
||||
assertUpdateModel(this.integerView, null, '90');
|
||||
assertUpdateModel(this.floatView, 10.2, '-9.5');
|
||||
});
|
||||
|
||||
it("knows the difference between integer and float", function() {
|
||||
expect(this.integerView.isIntegerField()).toBeTruthy();
|
||||
expect(this.floatView.isIntegerField()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("sets attribtues related to min, max, and step", function() {
|
||||
const verifyAttributes = function(view, min, step, max=null) {
|
||||
const inputEntry = view.$el.find('input');
|
||||
expect(Number(inputEntry.attr('min'))).toEqual(min);
|
||||
expect(Number(inputEntry.attr('step'))).toEqual(step);
|
||||
if (max === !null) {
|
||||
expect(Number(inputEntry.attr('max'))).toEqual(max);
|
||||
}
|
||||
};
|
||||
|
||||
verifyAttributes(this.integerView, 1, 1);
|
||||
verifyAttributes(this.floatView, 1.3, .1, 100.2);
|
||||
});
|
||||
|
||||
it("corrects values that are out of range", function() {
|
||||
verifyValueAfterChanged(this.integerView, '-4', '1');
|
||||
verifyValueAfterChanged(this.integerView, '1', '1');
|
||||
verifyValueAfterChanged(this.integerView, '0', '1');
|
||||
verifyValueAfterChanged(this.integerView, '3001', '3001');
|
||||
|
||||
verifyValueAfterChanged(this.floatView, '-4', '1.3');
|
||||
verifyValueAfterChanged(this.floatView, '1.3', '1.3');
|
||||
verifyValueAfterChanged(this.floatView, '1.2', '1.3');
|
||||
verifyValueAfterChanged(this.floatView, '100.2', '100.2');
|
||||
verifyValueAfterChanged(this.floatView, '100.3', '100.2');
|
||||
});
|
||||
|
||||
it("sets default values for integer and float fields that are empty", function() {
|
||||
verifyValueAfterChanged(this.integerView, '', '6');
|
||||
verifyValueAfterChanged(this.floatView, '', '2.7');
|
||||
});
|
||||
|
||||
it("disallows invalid characters", function() {
|
||||
const verifyValueAfterKeyPressed = function(view, character, reject) {
|
||||
const event = {
|
||||
type : 'keypress',
|
||||
which : character.charCodeAt(0),
|
||||
keyCode: character.charCodeAt(0),
|
||||
preventDefault() { return 'no op'; }
|
||||
};
|
||||
spyOn(event, 'preventDefault');
|
||||
view.$el.find('input').trigger(event);
|
||||
if (reject) {
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
} else {
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
}
|
||||
};
|
||||
|
||||
const verifyDisallowedChars = function(view) {
|
||||
verifyValueAfterKeyPressed(view, 'a', true);
|
||||
verifyValueAfterKeyPressed(view, '.', view.isIntegerField());
|
||||
verifyValueAfterKeyPressed(view, '[', true);
|
||||
verifyValueAfterKeyPressed(view, '@', true);
|
||||
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8].map((i) =>
|
||||
verifyValueAfterKeyPressed(view, String(i), false));
|
||||
};
|
||||
|
||||
verifyDisallowedChars(this.integerView);
|
||||
verifyDisallowedChars(this.floatView);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MetadataView.List allows the user to enter an ordered list of strings", function() {
|
||||
beforeEach(function() {
|
||||
const listModel = new MetadataModel(listEntry);
|
||||
this.listView = new MetadataView.List({model: listModel});
|
||||
this.el = this.listView.$el;
|
||||
main();
|
||||
});
|
||||
|
||||
it("returns the initial value upon initialization", function() {
|
||||
assertValueInView(this.listView, ['the first display value', 'the second']);
|
||||
});
|
||||
|
||||
it("updates its value correctly", function() {
|
||||
assertCanUpdateView(this.listView, ['a new item', 'another new item', 'a third']);
|
||||
});
|
||||
|
||||
it("has a clear method to revert to the model default", function() {
|
||||
this.el.find('.create-setting').click();
|
||||
assertClear(this.listView, ['a thing', 'another thing']);
|
||||
expect(this.el.find('.create-setting')).not.toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
it("has an update model method", function() {
|
||||
assertUpdateModel(this.listView, null, ['a new value']);
|
||||
});
|
||||
|
||||
it("can add an entry", function() {
|
||||
expect(this.listView.model.get('value').length).toEqual(2);
|
||||
this.el.find('.create-setting').click();
|
||||
expect(this.el.find('input.input').length).toEqual(3);
|
||||
});
|
||||
|
||||
it("can remove an entry", function() {
|
||||
expect(this.listView.model.get('value').length).toEqual(2);
|
||||
this.el.find('.remove-setting').first().click();
|
||||
expect(this.listView.model.get('value').length).toEqual(1);
|
||||
});
|
||||
|
||||
it("only allows one blank entry at a time", function() {
|
||||
expect(this.el.find('input').length).toEqual(2);
|
||||
this.el.find('.create-setting').click();
|
||||
this.el.find('.create-setting').click();
|
||||
expect(this.el.find('input').length).toEqual(3);
|
||||
});
|
||||
|
||||
it("re-enables the add setting button after entering a new value", function() {
|
||||
expect(this.el.find('input').length).toEqual(2);
|
||||
this.el.find('.create-setting').click();
|
||||
expect(this.el.find('.create-setting')).toHaveClass('is-disabled');
|
||||
this.el.find('input').last().val('third setting');
|
||||
this.el.find('input').last().trigger('input');
|
||||
expect(this.el.find('.create-setting')).not.toHaveClass('is-disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe("MetadataView.RelativeTime allows the user to enter time string in HH:mm:ss format", function() {
|
||||
beforeEach(function() {
|
||||
const model = new MetadataModel(timeEntry);
|
||||
this.view = new MetadataView.RelativeTime({model});
|
||||
});
|
||||
|
||||
it("uses a text input type", function() {
|
||||
assertInputType(this.view, 'text');
|
||||
});
|
||||
|
||||
it("returns the intial value upon initialization", function() {
|
||||
assertValueInView(this.view, '12:12:12');
|
||||
});
|
||||
|
||||
it("value is converted correctly", function() {
|
||||
const { view } = this;
|
||||
|
||||
const cases = [
|
||||
{
|
||||
input: '23:100:0',
|
||||
output: '23:59:59'
|
||||
},
|
||||
{
|
||||
input: '100000000000000000',
|
||||
output: '23:59:59'
|
||||
},
|
||||
{
|
||||
input: '80000',
|
||||
output: '22:13:20'
|
||||
},
|
||||
{
|
||||
input: '-100',
|
||||
output: '00:00:00'
|
||||
},
|
||||
{
|
||||
input: '-100:-10',
|
||||
output: '00:00:00'
|
||||
},
|
||||
{
|
||||
input: '99:99',
|
||||
output: '01:40:39'
|
||||
},
|
||||
{
|
||||
input: '2',
|
||||
output: '00:00:02'
|
||||
},
|
||||
{
|
||||
input: '1:2',
|
||||
output: '00:01:02'
|
||||
},
|
||||
{
|
||||
input: '1:25',
|
||||
output: '00:01:25'
|
||||
},
|
||||
{
|
||||
input: '3:1:25',
|
||||
output: '03:01:25'
|
||||
},
|
||||
{
|
||||
input: ' 2 3 : 5 9 : 5 9 ',
|
||||
output: '23:59:59'
|
||||
},
|
||||
{
|
||||
input: '9:1:25',
|
||||
output: '09:01:25'
|
||||
},
|
||||
{
|
||||
input: '77:72:77',
|
||||
output: '23:59:59'
|
||||
},
|
||||
{
|
||||
input: '22:100:100',
|
||||
output: '23:41:40'
|
||||
},
|
||||
// negative value
|
||||
{
|
||||
input: '-22:22:22',
|
||||
output: '00:22:22'
|
||||
},
|
||||
// simple string
|
||||
{
|
||||
input: 'simple text',
|
||||
output: '00:00:00'
|
||||
},
|
||||
{
|
||||
input: 'a10a:a10a:a10a',
|
||||
output: '00:00:00'
|
||||
},
|
||||
// empty string
|
||||
{
|
||||
input: '',
|
||||
output: '00:00:00'
|
||||
}
|
||||
];
|
||||
|
||||
$.each(cases, (index, data) => expect(view.parseRelativeTime(data.input)).toBe(data.output));
|
||||
});
|
||||
|
||||
it("can update its value in the view", function() {
|
||||
assertCanUpdateView(this.view, "23:59:59");
|
||||
this.view.setValueInEditor("33:59:59");
|
||||
this.view.updateModel();
|
||||
assertValueInView(this.view, "23:59:59");
|
||||
});
|
||||
|
||||
it("has a clear method to revert to the model default", function() {
|
||||
assertClear(this.view, '00:00:00');
|
||||
});
|
||||
|
||||
it("has an update model method", function() {
|
||||
assertUpdateModel(this.view, '12:12:12', '23:59:59');
|
||||
});
|
||||
});
|
||||
|
||||
describe("MetadataView.Dict allows the user to enter key-value pairs of strings", function() {
|
||||
beforeEach(function() {
|
||||
const dictModel = new MetadataModel($.extend(true, {}, dictEntry));
|
||||
this.dictView = new MetadataView.Dict({model: dictModel});
|
||||
this.el = this.dictView.$el;
|
||||
main();
|
||||
});
|
||||
|
||||
it("returns the initial value upon initialization", function() {
|
||||
assertValueInView(this.dictView, {
|
||||
'en': 'English',
|
||||
'ru': 'Русский',
|
||||
'ua': 'Українська',
|
||||
'fr': 'Français'
|
||||
});
|
||||
});
|
||||
|
||||
it("updates its value correctly", function() {
|
||||
assertCanUpdateView(this.dictView, {
|
||||
'ru': 'Русский',
|
||||
'ua': 'Українська',
|
||||
'fr': 'Français'
|
||||
});
|
||||
});
|
||||
|
||||
it("has a clear method to revert to the model default", function() {
|
||||
this.el.find('.create-setting').click();
|
||||
assertClear(this.dictView, {
|
||||
'en': 'English',
|
||||
'ru': 'Русский'
|
||||
});
|
||||
expect(this.el.find('.create-setting')).not.toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
it("has an update model method", function() {
|
||||
assertUpdateModel(this.dictView, null, {'fr': 'Français'});
|
||||
});
|
||||
|
||||
it("can add an entry", function() {
|
||||
expect(_.keys(this.dictView.model.get('value')).length).toEqual(4);
|
||||
this.el.find('.create-setting').click();
|
||||
expect(this.el.find('input.input-key').length).toEqual(5);
|
||||
});
|
||||
|
||||
it("can remove an entry", function() {
|
||||
expect(_.keys(this.dictView.model.get('value')).length).toEqual(4);
|
||||
this.el.find('.remove-setting').first().click();
|
||||
expect(_.keys(this.dictView.model.get('value')).length).toEqual(3);
|
||||
});
|
||||
|
||||
it("only allows one blank entry at a time", function() {
|
||||
expect(this.el.find('input.input-key').length).toEqual(4);
|
||||
this.el.find('.create-setting').click();
|
||||
this.el.find('.create-setting').click();
|
||||
expect(this.el.find('input.input-key').length).toEqual(5);
|
||||
});
|
||||
|
||||
it("only allows unique keys", function() {
|
||||
const data = [
|
||||
{
|
||||
expectedValue: {'ru': 'Русский'},
|
||||
initialValue: {'ru': 'Русский'},
|
||||
testValue: {
|
||||
'key': 'ru',
|
||||
'value': ''
|
||||
}
|
||||
},
|
||||
{
|
||||
expectedValue: {'ru': 'Русский'},
|
||||
initialValue: {'ru': 'Some value'},
|
||||
testValue: {
|
||||
'key': 'ru',
|
||||
'value': 'Русский'
|
||||
}
|
||||
},
|
||||
{
|
||||
expectedValue: {'ru': 'Русский'},
|
||||
initialValue: {'ru': 'Русский'},
|
||||
testValue: {
|
||||
'key': '',
|
||||
'value': ''
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
_.each(data, ((d, index) => {
|
||||
this.dictView.setValueInEditor(d.initialValue);
|
||||
this.dictView.updateModel();
|
||||
this.el.find('.create-setting').click();
|
||||
const item = this.el.find('.list-settings-item').last();
|
||||
item.find('.input-key').val(d.testValue.key);
|
||||
item.find('.input-value').val(d.testValue.value);
|
||||
|
||||
expect(this.dictView.getValueFromEditor()).toEqual(d.expectedValue);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("re-enables the add setting button after entering a new value", function() {
|
||||
expect(this.el.find('input.input-key').length).toEqual(4);
|
||||
this.el.find('.create-setting').click();
|
||||
expect(this.el.find('.create-setting')).toHaveClass('is-disabled');
|
||||
this.el.find('input.input-key').last().val('third setting');
|
||||
this.el.find('input.input-key').last().trigger('input');
|
||||
expect(this.el.find('.create-setting')).not.toHaveClass('is-disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,341 +0,0 @@
|
||||
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", "common/js/components/views/feedback_prompt",
|
||||
"common/js/components/views/feedback_notification", "common/js/components/utils/view_utils",
|
||||
"edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers",
|
||||
"js/spec_helpers/modal_helpers"],
|
||||
(Textbook, Chapter, ChapterSet, Course, TextbookSet, ShowTextbook, EditTextbook, ListTextbooks, EditChapter,
|
||||
Prompt, Notification, ViewUtils, AjaxHelpers, modal_helpers) ->
|
||||
|
||||
describe "ShowTextbook", ->
|
||||
tpl = readFixtures('show-textbook.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "show-textbook-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures(sandbox({id: "page-notification"}))
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}))
|
||||
@model = new Textbook({name: "Life Sciences", id: "0life-sciences"})
|
||||
spyOn(@model, "destroy").and.callThrough()
|
||||
@collection = new TextbookSet([@model])
|
||||
@view = new ShowTextbook({model: @model})
|
||||
|
||||
@promptSpies = jasmine.stealth.spyOnConstructor(Prompt, "Warning", ["show", "hide"])
|
||||
@promptSpies.show.and.returnValue(@promptSpies)
|
||||
window.course = new Course({
|
||||
id: "5",
|
||||
name: "Course Name",
|
||||
url_name: "course_name",
|
||||
org: "course_org",
|
||||
num: "course_num",
|
||||
revision: "course_rev"
|
||||
});
|
||||
|
||||
afterEach ->
|
||||
delete window.course
|
||||
|
||||
describe "Basic", ->
|
||||
it "should render properly", ->
|
||||
@view.render()
|
||||
expect(@view.$el).toContainText("Life Sciences")
|
||||
|
||||
it "should set the 'editing' property on the model when the edit button is clicked", ->
|
||||
@view.render().$(".edit").click()
|
||||
expect(@model.get("editing")).toBeTruthy()
|
||||
|
||||
it "should pop a delete confirmation when the delete button is clicked", ->
|
||||
@view.render().$(".delete").click()
|
||||
expect(@promptSpies.constructor).toHaveBeenCalled()
|
||||
ctorOptions = @promptSpies.constructor.calls.mostRecent().args[0]
|
||||
expect(ctorOptions.title).toMatch(/Life Sciences/)
|
||||
# hasn't actually been removed
|
||||
expect(@model.destroy).not.toHaveBeenCalled()
|
||||
expect(@collection).toContain(@model)
|
||||
|
||||
it "should show chapters appropriately", ->
|
||||
@model.get("chapters").add([{}, {}, {}])
|
||||
@model.set('showChapters', false)
|
||||
@view.render().$(".show-chapters").click()
|
||||
expect(@model.get('showChapters')).toBeTruthy()
|
||||
|
||||
it "should hide chapters appropriately", ->
|
||||
@model.get("chapters").add([{}, {}, {}])
|
||||
@model.set('showChapters', true)
|
||||
@view.render().$(".hide-chapters").click()
|
||||
expect(@model.get('showChapters')).toBeFalsy()
|
||||
|
||||
describe "AJAX", ->
|
||||
beforeEach ->
|
||||
@savingSpies = jasmine.stealth.spyOnConstructor(Notification, "Mini",
|
||||
["show", "hide"])
|
||||
@savingSpies.show.and.returnValue(@savingSpies)
|
||||
CMS.URL.TEXTBOOKS = "/textbooks"
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.TEXTBOOKS
|
||||
|
||||
it "should destroy itself on confirmation", ->
|
||||
requests = AjaxHelpers["requests"](this)
|
||||
|
||||
@view.render().$(".delete").click()
|
||||
ctorOptions = @promptSpies.constructor.calls.mostRecent().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(@savingSpies.constructor).toHaveBeenCalled()
|
||||
expect(@savingSpies.show).toHaveBeenCalled()
|
||||
expect(@savingSpies.hide).not.toHaveBeenCalled()
|
||||
savingOptions = @savingSpies.constructor.calls.mostRecent().args[0]
|
||||
expect(savingOptions.title).toMatch(/Deleting/)
|
||||
# return a success response
|
||||
requests[0].respond(204)
|
||||
expect(@savingSpies.hide).toHaveBeenCalled()
|
||||
expect(@collection.contains(@model)).toBeFalsy()
|
||||
|
||||
describe "EditTextbook", ->
|
||||
describe "Basic", ->
|
||||
tpl = readFixtures('edit-textbook.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "edit-textbook-tpl", type: "text/template"}).text(tpl))
|
||||
appendSetFixtures(sandbox({id: "page-notification"}))
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}))
|
||||
@model = new Textbook({name: "Life Sciences", editing: true})
|
||||
spyOn(@model, 'save')
|
||||
@collection = new TextbookSet()
|
||||
@collection.add(@model)
|
||||
@view = new EditTextbook({model: @model})
|
||||
spyOn(@view, 'render').and.callThrough()
|
||||
|
||||
it "should render properly", ->
|
||||
@view.render()
|
||||
expect(@view.$("input[name=textbook-name]").val()).toEqual("Life Sciences")
|
||||
|
||||
it "should allow you to create new empty chapters", ->
|
||||
@view.render()
|
||||
numChapters = @model.get("chapters").length
|
||||
@view.$(".action-add-chapter").click()
|
||||
expect(@model.get("chapters").length).toEqual(numChapters+1)
|
||||
expect(@model.get("chapters").last().isEmpty()).toBeTruthy()
|
||||
|
||||
it "should save properly", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-name]").val("wallflower")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar")
|
||||
@view.$("form").submit()
|
||||
expect(@model.get("name")).toEqual("starfish")
|
||||
chapter = @model.get("chapters").first()
|
||||
expect(chapter.get("name")).toEqual("wallflower")
|
||||
expect(chapter.get("asset_path")).toEqual("foobar")
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
it "should not save on invalid", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeTruthy()
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
|
||||
it "does not save on cancel", ->
|
||||
@model.get("chapters").add([{name: "a", asset_path: "b"}])
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$(".action-cancel").click()
|
||||
expect(@model.get("name")).not.toEqual("starfish")
|
||||
chapter = @model.get("chapters").first()
|
||||
expect(chapter.get("asset_path")).not.toEqual("foobar")
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
|
||||
it "should be possible to correct validation errors", ->
|
||||
@view.render()
|
||||
@view.$("input[name=textbook-name]").val("")
|
||||
@view.$("input[name=chapter1-asset-path]").val("foobar.pdf")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeTruthy()
|
||||
expect(@model.save).not.toHaveBeenCalled()
|
||||
@view.$("input[name=textbook-name]").val("starfish")
|
||||
@view.$("input[name=chapter1-name]").val("foobar")
|
||||
@view.$("form").submit()
|
||||
expect(@model.validationError).toBeFalsy()
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
it "removes all empty chapters on cancel if the model has a non-empty chapter", ->
|
||||
chapters = @model.get("chapters")
|
||||
chapters.at(0).set("name", "non-empty")
|
||||
@model.setOriginalAttributes()
|
||||
@view.render()
|
||||
chapters.add([{}, {}, {}]) # add three empty chapters
|
||||
expect(chapters.length).toEqual(4)
|
||||
@view.$(".action-cancel").click()
|
||||
expect(chapters.length).toEqual(1)
|
||||
expect(chapters.first().get('name')).toEqual("non-empty")
|
||||
|
||||
it "removes all empty chapters on cancel except one if the model has no non-empty chapters", ->
|
||||
chapters = @model.get("chapters")
|
||||
@view.render()
|
||||
chapters.add([{}, {}, {}]) # add three empty chapters
|
||||
expect(chapters.length).toEqual(4)
|
||||
@view.$(".action-cancel").click()
|
||||
expect(chapters.length).toEqual(1)
|
||||
|
||||
describe "ListTextbooks", ->
|
||||
noTextbooksTpl = readFixtures("no-textbooks.underscore")
|
||||
editTextbooktpl = readFixtures('edit-textbook.underscore')
|
||||
|
||||
beforeEach ->
|
||||
appendSetFixtures($("<script>", {id: "no-textbooks-tpl", type: "text/template"}).text(noTextbooksTpl))
|
||||
appendSetFixtures($("<script>", {id: "edit-textbook-tpl", type: "text/template"}).text(editTextbooktpl))
|
||||
@collection = new TextbookSet
|
||||
@view = new ListTextbooks({collection: @collection})
|
||||
@view.render()
|
||||
|
||||
it "should scroll to newly added textbook", ->
|
||||
spyOn(ViewUtils, 'setScrollOffset')
|
||||
@view.$(".new-button").click()
|
||||
$sectionEl = @view.$el.find('section:last')
|
||||
expect($sectionEl.length).toEqual(1)
|
||||
expect(ViewUtils.setScrollOffset).toHaveBeenCalledWith($sectionEl, 0)
|
||||
|
||||
it "should focus first input element of newly added textbook", ->
|
||||
spyOn(jQuery.fn, 'focus').and.callThrough()
|
||||
jasmine.addMatchers
|
||||
toHaveBeenCalledOnJQueryObject: () ->
|
||||
return {
|
||||
compare: (actual, expected) ->
|
||||
return {
|
||||
pass: actual.calls && actual.calls.mostRecent() &&
|
||||
actual.calls.mostRecent().object[0] == expected[0]
|
||||
}
|
||||
}
|
||||
@view.$(".new-button").click()
|
||||
$inputEl = @view.$el.find('section:last input:first')
|
||||
expect($inputEl.length).toEqual(1)
|
||||
# testing for element focused seems to be tricky
|
||||
# (see http://stackoverflow.com/questions/967096)
|
||||
# and the following doesn't seem to work
|
||||
# expect($inputEl).toBeFocused()
|
||||
# expect($inputEl.find(':focus').length).toEqual(1)
|
||||
expect(jQuery.fn.focus).toHaveBeenCalledOnJQueryObject($inputEl)
|
||||
|
||||
# describe "ListTextbooks", ->
|
||||
# noTextbooksTpl = readFixtures("no-textbooks.underscore")
|
||||
#
|
||||
# beforeEach ->
|
||||
# setFixtures($("<script>", {id: "no-textbooks-tpl", type: "text/template"}).text(noTextbooksTpl))
|
||||
# @showSpies = spyOnConstructor("ShowTextbook", ["render"])
|
||||
# @showSpies.render.and.returnValue(@showSpies) # equivalent of `return this`
|
||||
# showEl = $("<li>")
|
||||
# @showSpies.$el = showEl
|
||||
# @showSpies.el = showEl.get(0)
|
||||
# @editSpies = spyOnConstructor("EditTextbook", ["render"])
|
||||
# editEl = $("<li>")
|
||||
# @editSpies.render.and.returnValue(@editSpies)
|
||||
# @editSpies.$el = editEl
|
||||
# @editSpies.el= editEl.get(0)
|
||||
#
|
||||
# @collection = new TextbookSet
|
||||
# @view = new ListTextbooks({collection: @collection})
|
||||
# @view.render()
|
||||
#
|
||||
# it "should render the empty template if there are no textbooks", ->
|
||||
# expect(@view.$el).toContainText("You haven't added any textbooks to this course yet")
|
||||
# expect(@view.$el).toContain(".new-button")
|
||||
# expect(@showSpies.constructor).not.toHaveBeenCalled()
|
||||
# expect(@editSpies.constructor).not.toHaveBeenCalled()
|
||||
#
|
||||
# it "should render ShowTextbook views by default if no textbook is being edited", ->
|
||||
# # add three empty textbooks to the collection
|
||||
# @collection.add([{}, {}, {}])
|
||||
# # reset spies due to re-rendering on collection modification
|
||||
# @showSpies.constructor.reset()
|
||||
# @editSpies.constructor.reset()
|
||||
# # render once and test
|
||||
# @view.render()
|
||||
#
|
||||
# expect(@view.$el).not.toContainText(
|
||||
# "You haven't added any textbooks to this course yet")
|
||||
# expect(@showSpies.constructor).toHaveBeenCalled()
|
||||
# expect(@showSpies.constructor.calls.length).toEqual(3);
|
||||
# expect(@editSpies.constructor).not.toHaveBeenCalled()
|
||||
#
|
||||
# it "should render an EditTextbook view for a textbook being edited", ->
|
||||
# # add three empty textbooks to the collection: the first and third
|
||||
# # should be shown, and the second should be edited
|
||||
# @collection.add([{editing: false}, {editing: true}, {editing: false}])
|
||||
# editing = @collection.at(1)
|
||||
# expect(editing.get("editing")).toBeTruthy()
|
||||
# # reset spies
|
||||
# @showSpies.constructor.reset()
|
||||
# @editSpies.constructor.reset()
|
||||
# # render once and test
|
||||
# @view.render()
|
||||
#
|
||||
# expect(@showSpies.constructor).toHaveBeenCalled()
|
||||
# expect(@showSpies.constructor.calls.length).toEqual(2)
|
||||
# expect(@showSpies.constructor).not.toHaveBeenCalledWith({model: editing})
|
||||
# expect(@editSpies.constructor).toHaveBeenCalled()
|
||||
# expect(@editSpies.constructor.calls.length).toEqual(1)
|
||||
# expect(@editSpies.constructor).toHaveBeenCalledWith({model: editing})
|
||||
#
|
||||
# it "should add a new textbook when the new-button is clicked", ->
|
||||
# # reset spies
|
||||
# @showSpies.constructor.reset()
|
||||
# @editSpies.constructor.reset()
|
||||
# # test
|
||||
# @view.$(".new-button").click()
|
||||
#
|
||||
# expect(@collection.length).toEqual(1)
|
||||
# expect(@view.$el).toContain(@editSpies.$el)
|
||||
# expect(@view.$el).not.toContain(@showSpies.$el)
|
||||
|
||||
|
||||
describe "EditChapter", ->
|
||||
beforeEach ->
|
||||
modal_helpers.installModalTemplates()
|
||||
@model = new Chapter
|
||||
name: "Chapter 1"
|
||||
asset_path: "/ch1.pdf"
|
||||
@collection = new ChapterSet()
|
||||
@collection.add(@model)
|
||||
@view = new EditChapter({model: @model})
|
||||
spyOn(@view, "remove").and.callThrough()
|
||||
CMS.URL.UPLOAD_ASSET = "/upload"
|
||||
window.course = new Course({name: "abcde"})
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.UPLOAD_ASSET
|
||||
delete window.course
|
||||
|
||||
it "can render", ->
|
||||
@view.render()
|
||||
expect(@view.$("input.chapter-name").val()).toEqual("Chapter 1")
|
||||
expect(@view.$("input.chapter-asset-path").val()).toEqual("/ch1.pdf")
|
||||
|
||||
it "can delete itself", ->
|
||||
@view.render().$(".action-close").click()
|
||||
expect(@collection.length).toEqual(0)
|
||||
expect(@view.remove).toHaveBeenCalled()
|
||||
|
||||
# it "can open an upload dialog", ->
|
||||
# uploadSpies = spyOnConstructor("UploadDialog", ["show", "el"])
|
||||
# uploadSpies.show.and.returnValue(uploadSpies)
|
||||
#
|
||||
# @view.render().$(".action-upload").click()
|
||||
# ctorOptions = uploadSpies.constructor.calls.mostRecent().args[0]
|
||||
# expect(ctorOptions.model.get('title')).toMatch(/abcde/)
|
||||
# expect(typeof ctorOptions.onSuccess).toBe('function')
|
||||
# expect(uploadSpies.show).toHaveBeenCalled()
|
||||
|
||||
# Disabling because this test does not close the modal dialog. This can cause
|
||||
# tests that run after it to fail (see STUD-1963).
|
||||
xit "saves content when opening upload dialog", ->
|
||||
@view.render()
|
||||
@view.$("input.chapter-name").val("rainbows")
|
||||
@view.$("input.chapter-asset-path").val("unicorns")
|
||||
@view.$(".action-upload").click()
|
||||
expect(@model.get("name")).toEqual("rainbows")
|
||||
expect(@model.get("asset_path")).toEqual("unicorns")
|
||||
375
cms/static/coffee/spec/views/textbook_spec.js
Normal file
375
cms/static/coffee/spec/views/textbook_spec.js
Normal file
@@ -0,0 +1,375 @@
|
||||
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", "common/js/components/views/feedback_prompt",
|
||||
"common/js/components/views/feedback_notification", "common/js/components/utils/view_utils",
|
||||
"edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers",
|
||||
"js/spec_helpers/modal_helpers"],
|
||||
function(Textbook, Chapter, ChapterSet, Course, TextbookSet, ShowTextbook, EditTextbook, ListTextbooks, EditChapter,
|
||||
Prompt, Notification, ViewUtils, AjaxHelpers, modal_helpers) {
|
||||
|
||||
describe("ShowTextbook", function() {
|
||||
const tpl = readFixtures('show-textbook.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures($("<script>", {id: "show-textbook-tpl", type: "text/template"}).text(tpl));
|
||||
appendSetFixtures(sandbox({id: "page-notification"}));
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}));
|
||||
this.model = new Textbook({name: "Life Sciences", id: "0life-sciences"});
|
||||
spyOn(this.model, "destroy").and.callThrough();
|
||||
this.collection = new TextbookSet([this.model]);
|
||||
this.view = new ShowTextbook({model: this.model});
|
||||
|
||||
this.promptSpies = jasmine.stealth.spyOnConstructor(Prompt, "Warning", ["show", "hide"]);
|
||||
this.promptSpies.show.and.returnValue(this.promptSpies);
|
||||
window.course = new Course({
|
||||
id: "5",
|
||||
name: "Course Name",
|
||||
url_name: "course_name",
|
||||
org: "course_org",
|
||||
num: "course_num",
|
||||
revision: "course_rev"
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => delete window.course);
|
||||
|
||||
describe("Basic", function() {
|
||||
it("should render properly", function() {
|
||||
this.view.render();
|
||||
expect(this.view.$el).toContainText("Life Sciences");
|
||||
});
|
||||
|
||||
it("should set the 'editing' property on the model when the edit button is clicked", function() {
|
||||
this.view.render().$(".edit").click();
|
||||
expect(this.model.get("editing")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should pop a delete confirmation when the delete button is clicked", function() {
|
||||
this.view.render().$(".delete").click();
|
||||
expect(this.promptSpies.constructor).toHaveBeenCalled();
|
||||
const ctorOptions = this.promptSpies.constructor.calls.mostRecent().args[0];
|
||||
expect(ctorOptions.title).toMatch(/Life Sciences/);
|
||||
// hasn't actually been removed
|
||||
expect(this.model.destroy).not.toHaveBeenCalled();
|
||||
expect(this.collection).toContain(this.model);
|
||||
});
|
||||
|
||||
it("should show chapters appropriately", function() {
|
||||
this.model.get("chapters").add([{}, {}, {}]);
|
||||
this.model.set('showChapters', false);
|
||||
this.view.render().$(".show-chapters").click();
|
||||
expect(this.model.get('showChapters')).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should hide chapters appropriately", function() {
|
||||
this.model.get("chapters").add([{}, {}, {}]);
|
||||
this.model.set('showChapters', true);
|
||||
this.view.render().$(".hide-chapters").click();
|
||||
expect(this.model.get('showChapters')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("AJAX", function() {
|
||||
beforeEach(function() {
|
||||
this.savingSpies = jasmine.stealth.spyOnConstructor(Notification, "Mini",
|
||||
["show", "hide"]);
|
||||
this.savingSpies.show.and.returnValue(this.savingSpies);
|
||||
CMS.URL.TEXTBOOKS = "/textbooks";
|
||||
});
|
||||
|
||||
afterEach(() => delete CMS.URL.TEXTBOOKS);
|
||||
|
||||
it("should destroy itself on confirmation", function() {
|
||||
const requests = AjaxHelpers["requests"](this);
|
||||
|
||||
this.view.render().$(".delete").click();
|
||||
const ctorOptions = this.promptSpies.constructor.calls.mostRecent().args[0];
|
||||
// run the primary function to indicate confirmation
|
||||
ctorOptions.actions.primary.click(this.promptSpies);
|
||||
// AJAX request has been sent, but not yet returned
|
||||
expect(this.model.destroy).toHaveBeenCalled();
|
||||
expect(requests.length).toEqual(1);
|
||||
expect(this.savingSpies.constructor).toHaveBeenCalled();
|
||||
expect(this.savingSpies.show).toHaveBeenCalled();
|
||||
expect(this.savingSpies.hide).not.toHaveBeenCalled();
|
||||
const savingOptions = this.savingSpies.constructor.calls.mostRecent().args[0];
|
||||
expect(savingOptions.title).toMatch(/Deleting/);
|
||||
// return a success response
|
||||
requests[0].respond(204);
|
||||
expect(this.savingSpies.hide).toHaveBeenCalled();
|
||||
expect(this.collection.contains(this.model)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("EditTextbook", () =>
|
||||
describe("Basic", function() {
|
||||
const tpl = readFixtures('edit-textbook.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures($("<script>", {id: "edit-textbook-tpl", type: "text/template"}).text(tpl));
|
||||
appendSetFixtures(sandbox({id: "page-notification"}));
|
||||
appendSetFixtures(sandbox({id: "page-prompt"}));
|
||||
this.model = new Textbook({name: "Life Sciences", editing: true});
|
||||
spyOn(this.model, 'save');
|
||||
this.collection = new TextbookSet();
|
||||
this.collection.add(this.model);
|
||||
this.view = new EditTextbook({model: this.model});
|
||||
spyOn(this.view, 'render').and.callThrough();
|
||||
});
|
||||
|
||||
it("should render properly", function() {
|
||||
this.view.render();
|
||||
expect(this.view.$("input[name=textbook-name]").val()).toEqual("Life Sciences");
|
||||
});
|
||||
|
||||
it("should allow you to create new empty chapters", function() {
|
||||
this.view.render();
|
||||
const numChapters = this.model.get("chapters").length;
|
||||
this.view.$(".action-add-chapter").click();
|
||||
expect(this.model.get("chapters").length).toEqual(numChapters+1);
|
||||
expect(this.model.get("chapters").last().isEmpty()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should save properly", function() {
|
||||
this.view.render();
|
||||
this.view.$("input[name=textbook-name]").val("starfish");
|
||||
this.view.$("input[name=chapter1-name]").val("wallflower");
|
||||
this.view.$("input[name=chapter1-asset-path]").val("foobar");
|
||||
this.view.$("form").submit();
|
||||
expect(this.model.get("name")).toEqual("starfish");
|
||||
const chapter = this.model.get("chapters").first();
|
||||
expect(chapter.get("name")).toEqual("wallflower");
|
||||
expect(chapter.get("asset_path")).toEqual("foobar");
|
||||
expect(this.model.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not save on invalid", function() {
|
||||
this.view.render();
|
||||
this.view.$("input[name=textbook-name]").val("");
|
||||
this.view.$("input[name=chapter1-asset-path]").val("foobar.pdf");
|
||||
this.view.$("form").submit();
|
||||
expect(this.model.validationError).toBeTruthy();
|
||||
expect(this.model.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not save on cancel", function() {
|
||||
this.model.get("chapters").add([{name: "a", asset_path: "b"}]);
|
||||
this.view.render();
|
||||
this.view.$("input[name=textbook-name]").val("starfish");
|
||||
this.view.$("input[name=chapter1-asset-path]").val("foobar.pdf");
|
||||
this.view.$(".action-cancel").click();
|
||||
expect(this.model.get("name")).not.toEqual("starfish");
|
||||
const chapter = this.model.get("chapters").first();
|
||||
expect(chapter.get("asset_path")).not.toEqual("foobar");
|
||||
expect(this.model.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should be possible to correct validation errors", function() {
|
||||
this.view.render();
|
||||
this.view.$("input[name=textbook-name]").val("");
|
||||
this.view.$("input[name=chapter1-asset-path]").val("foobar.pdf");
|
||||
this.view.$("form").submit();
|
||||
expect(this.model.validationError).toBeTruthy();
|
||||
expect(this.model.save).not.toHaveBeenCalled();
|
||||
this.view.$("input[name=textbook-name]").val("starfish");
|
||||
this.view.$("input[name=chapter1-name]").val("foobar");
|
||||
this.view.$("form").submit();
|
||||
expect(this.model.validationError).toBeFalsy();
|
||||
expect(this.model.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes all empty chapters on cancel if the model has a non-empty chapter", function() {
|
||||
const chapters = this.model.get("chapters");
|
||||
chapters.at(0).set("name", "non-empty");
|
||||
this.model.setOriginalAttributes();
|
||||
this.view.render();
|
||||
chapters.add([{}, {}, {}]); // add three empty chapters
|
||||
expect(chapters.length).toEqual(4);
|
||||
this.view.$(".action-cancel").click();
|
||||
expect(chapters.length).toEqual(1);
|
||||
expect(chapters.first().get('name')).toEqual("non-empty");
|
||||
});
|
||||
|
||||
it("removes all empty chapters on cancel except one if the model has no non-empty chapters", function() {
|
||||
const chapters = this.model.get("chapters");
|
||||
this.view.render();
|
||||
chapters.add([{}, {}, {}]); // add three empty chapters
|
||||
expect(chapters.length).toEqual(4);
|
||||
this.view.$(".action-cancel").click();
|
||||
expect(chapters.length).toEqual(1);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
describe("ListTextbooks", function() {
|
||||
const noTextbooksTpl = readFixtures("no-textbooks.underscore");
|
||||
const editTextbooktpl = readFixtures('edit-textbook.underscore');
|
||||
|
||||
beforeEach(function() {
|
||||
appendSetFixtures($("<script>", {id: "no-textbooks-tpl", type: "text/template"}).text(noTextbooksTpl));
|
||||
appendSetFixtures($("<script>", {id: "edit-textbook-tpl", type: "text/template"}).text(editTextbooktpl));
|
||||
this.collection = new TextbookSet;
|
||||
this.view = new ListTextbooks({collection: this.collection});
|
||||
this.view.render();
|
||||
});
|
||||
|
||||
it("should scroll to newly added textbook", function() {
|
||||
spyOn(ViewUtils, 'setScrollOffset');
|
||||
this.view.$(".new-button").click();
|
||||
const $sectionEl = this.view.$el.find('section:last');
|
||||
expect($sectionEl.length).toEqual(1);
|
||||
expect(ViewUtils.setScrollOffset).toHaveBeenCalledWith($sectionEl, 0);
|
||||
});
|
||||
|
||||
it("should focus first input element of newly added textbook", function() {
|
||||
spyOn(jQuery.fn, 'focus').and.callThrough();
|
||||
jasmine.addMatchers({
|
||||
toHaveBeenCalledOnJQueryObject() {
|
||||
return {
|
||||
compare(actual, expected) {
|
||||
return {
|
||||
pass: actual.calls && actual.calls.mostRecent() &&
|
||||
(actual.calls.mostRecent().object[0] === expected[0])
|
||||
};
|
||||
}
|
||||
};
|
||||
}});
|
||||
this.view.$(".new-button").click();
|
||||
const $inputEl = this.view.$el.find('section:last input:first');
|
||||
expect($inputEl.length).toEqual(1);
|
||||
// testing for element focused seems to be tricky
|
||||
// (see http://stackoverflow.com/questions/967096)
|
||||
// and the following doesn't seem to work
|
||||
// expect($inputEl).toBeFocused()
|
||||
// expect($inputEl.find(':focus').length).toEqual(1)
|
||||
expect(jQuery.fn.focus).toHaveBeenCalledOnJQueryObject($inputEl);
|
||||
});
|
||||
});
|
||||
|
||||
// describe "ListTextbooks", ->
|
||||
// noTextbooksTpl = readFixtures("no-textbooks.underscore")
|
||||
//
|
||||
// beforeEach ->
|
||||
// setFixtures($("<script>", {id: "no-textbooks-tpl", type: "text/template"}).text(noTextbooksTpl))
|
||||
// @showSpies = spyOnConstructor("ShowTextbook", ["render"])
|
||||
// @showSpies.render.and.returnValue(@showSpies) # equivalent of `return this`
|
||||
// showEl = $("<li>")
|
||||
// @showSpies.$el = showEl
|
||||
// @showSpies.el = showEl.get(0)
|
||||
// @editSpies = spyOnConstructor("EditTextbook", ["render"])
|
||||
// editEl = $("<li>")
|
||||
// @editSpies.render.and.returnValue(@editSpies)
|
||||
// @editSpies.$el = editEl
|
||||
// @editSpies.el= editEl.get(0)
|
||||
//
|
||||
// @collection = new TextbookSet
|
||||
// @view = new ListTextbooks({collection: @collection})
|
||||
// @view.render()
|
||||
//
|
||||
// it "should render the empty template if there are no textbooks", ->
|
||||
// expect(@view.$el).toContainText("You haven't added any textbooks to this course yet")
|
||||
// expect(@view.$el).toContain(".new-button")
|
||||
// expect(@showSpies.constructor).not.toHaveBeenCalled()
|
||||
// expect(@editSpies.constructor).not.toHaveBeenCalled()
|
||||
//
|
||||
// it "should render ShowTextbook views by default if no textbook is being edited", ->
|
||||
// # add three empty textbooks to the collection
|
||||
// @collection.add([{}, {}, {}])
|
||||
// # reset spies due to re-rendering on collection modification
|
||||
// @showSpies.constructor.reset()
|
||||
// @editSpies.constructor.reset()
|
||||
// # render once and test
|
||||
// @view.render()
|
||||
//
|
||||
// expect(@view.$el).not.toContainText(
|
||||
// "You haven't added any textbooks to this course yet")
|
||||
// expect(@showSpies.constructor).toHaveBeenCalled()
|
||||
// expect(@showSpies.constructor.calls.length).toEqual(3);
|
||||
// expect(@editSpies.constructor).not.toHaveBeenCalled()
|
||||
//
|
||||
// it "should render an EditTextbook view for a textbook being edited", ->
|
||||
// # add three empty textbooks to the collection: the first and third
|
||||
// # should be shown, and the second should be edited
|
||||
// @collection.add([{editing: false}, {editing: true}, {editing: false}])
|
||||
// editing = @collection.at(1)
|
||||
// expect(editing.get("editing")).toBeTruthy()
|
||||
// # reset spies
|
||||
// @showSpies.constructor.reset()
|
||||
// @editSpies.constructor.reset()
|
||||
// # render once and test
|
||||
// @view.render()
|
||||
//
|
||||
// expect(@showSpies.constructor).toHaveBeenCalled()
|
||||
// expect(@showSpies.constructor.calls.length).toEqual(2)
|
||||
// expect(@showSpies.constructor).not.toHaveBeenCalledWith({model: editing})
|
||||
// expect(@editSpies.constructor).toHaveBeenCalled()
|
||||
// expect(@editSpies.constructor.calls.length).toEqual(1)
|
||||
// expect(@editSpies.constructor).toHaveBeenCalledWith({model: editing})
|
||||
//
|
||||
// it "should add a new textbook when the new-button is clicked", ->
|
||||
// # reset spies
|
||||
// @showSpies.constructor.reset()
|
||||
// @editSpies.constructor.reset()
|
||||
// # test
|
||||
// @view.$(".new-button").click()
|
||||
//
|
||||
// expect(@collection.length).toEqual(1)
|
||||
// expect(@view.$el).toContain(@editSpies.$el)
|
||||
// expect(@view.$el).not.toContain(@showSpies.$el)
|
||||
|
||||
|
||||
describe("EditChapter", function() {
|
||||
beforeEach(function() {
|
||||
modal_helpers.installModalTemplates();
|
||||
this.model = new Chapter({
|
||||
name: "Chapter 1",
|
||||
asset_path: "/ch1.pdf"
|
||||
});
|
||||
this.collection = new ChapterSet();
|
||||
this.collection.add(this.model);
|
||||
this.view = new EditChapter({model: this.model});
|
||||
spyOn(this.view, "remove").and.callThrough();
|
||||
CMS.URL.UPLOAD_ASSET = "/upload";
|
||||
window.course = new Course({name: "abcde"});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
delete CMS.URL.UPLOAD_ASSET;
|
||||
delete window.course;
|
||||
});
|
||||
|
||||
it("can render", function() {
|
||||
this.view.render();
|
||||
expect(this.view.$("input.chapter-name").val()).toEqual("Chapter 1");
|
||||
expect(this.view.$("input.chapter-asset-path").val()).toEqual("/ch1.pdf");
|
||||
});
|
||||
|
||||
it("can delete itself", function() {
|
||||
this.view.render().$(".action-close").click();
|
||||
expect(this.collection.length).toEqual(0);
|
||||
expect(this.view.remove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// it "can open an upload dialog", ->
|
||||
// uploadSpies = spyOnConstructor("UploadDialog", ["show", "el"])
|
||||
// uploadSpies.show.and.returnValue(uploadSpies)
|
||||
//
|
||||
// @view.render().$(".action-upload").click()
|
||||
// ctorOptions = uploadSpies.constructor.calls.mostRecent().args[0]
|
||||
// expect(ctorOptions.model.get('title')).toMatch(/abcde/)
|
||||
// expect(typeof ctorOptions.onSuccess).toBe('function')
|
||||
// expect(uploadSpies.show).toHaveBeenCalled()
|
||||
|
||||
// Disabling because this test does not close the modal dialog. This can cause
|
||||
// tests that run after it to fail (see STUD-1963).
|
||||
xit("saves content when opening upload dialog", function() {
|
||||
this.view.render();
|
||||
this.view.$("input.chapter-name").val("rainbows");
|
||||
this.view.$("input.chapter-asset-path").val("unicorns");
|
||||
this.view.$(".action-upload").click();
|
||||
expect(this.model.get("name")).toEqual("rainbows");
|
||||
expect(this.model.get("asset_path")).toEqual("unicorns");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,135 +0,0 @@
|
||||
define ["sinon", "js/models/uploads", "js/views/uploads", "js/models/chapter",
|
||||
"edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "js/spec_helpers/modal_helpers"],
|
||||
(sinon, FileUpload, UploadDialog, Chapter, AjaxHelpers, modal_helpers) ->
|
||||
|
||||
describe "UploadDialog", ->
|
||||
tpl = readFixtures("upload-dialog.underscore")
|
||||
|
||||
beforeEach ->
|
||||
modal_helpers.installModalTemplates()
|
||||
appendSetFixtures($("<script>", {id: "upload-dialog-tpl", type: "text/template"}).text(tpl))
|
||||
CMS.URL.UPLOAD_ASSET = "/upload"
|
||||
@model = new FileUpload(
|
||||
mimeTypes: ['application/pdf']
|
||||
)
|
||||
@dialogResponse = dialogResponse = []
|
||||
@mockFiles = []
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.UPLOAD_ASSET
|
||||
modal_helpers.cancelModalIfShowing()
|
||||
|
||||
createTestView = (test) ->
|
||||
view = new UploadDialog(
|
||||
model: test.model,
|
||||
url: CMS.URL.UPLOAD_ASSET,
|
||||
onSuccess: (response) =>
|
||||
test.dialogResponse.push(response.response)
|
||||
)
|
||||
spyOn(view, 'remove').and.callThrough()
|
||||
|
||||
# create mock file input, so that we aren't subject to browser restrictions
|
||||
mockFileInput = jasmine.createSpy('mockFileInput')
|
||||
mockFileInput.files = test.mockFiles
|
||||
jqMockFileInput = jasmine.createSpyObj('jqMockFileInput', ['get', 'replaceWith'])
|
||||
jqMockFileInput.get.and.returnValue(mockFileInput)
|
||||
originalView$ = view.$
|
||||
spyOn(view, "$").and.callFake (selector) ->
|
||||
if selector == "input[type=file]"
|
||||
jqMockFileInput
|
||||
else
|
||||
originalView$.apply(this, arguments)
|
||||
@lastView = view
|
||||
|
||||
describe "Basic", ->
|
||||
it "should render without a file selected", ->
|
||||
view = createTestView(this)
|
||||
view.render()
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$(".action-upload")).toHaveClass("disabled")
|
||||
|
||||
it "should render with a PDF selected", ->
|
||||
view = createTestView(this)
|
||||
file = {name: "fake.pdf", "type": "application/pdf"}
|
||||
@mockFiles.push(file)
|
||||
@model.set("selectedFile", file)
|
||||
view.render()
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$el).not.toContainElement("#upload_error")
|
||||
expect(view.$(".action-upload")).not.toHaveClass("disabled")
|
||||
|
||||
it "should render an error with an invalid file type selected", ->
|
||||
view = createTestView(this)
|
||||
file = {name: "fake.png", "type": "image/png"}
|
||||
@mockFiles.push(file)
|
||||
@model.set("selectedFile", file)
|
||||
view.render()
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$el).toContainElement("#upload_error")
|
||||
expect(view.$(".action-upload")).toHaveClass("disabled")
|
||||
|
||||
it "should render an error with an invalid file type after a correct file type selected", ->
|
||||
view = createTestView(this)
|
||||
correctFile = {name: "fake.pdf", "type": "application/pdf"}
|
||||
inCorrectFile = {name: "fake.png", "type": "image/png"}
|
||||
event = {}
|
||||
view.render()
|
||||
|
||||
event.target = {"files": [correctFile]}
|
||||
view.selectFile(event)
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$el).not.toContainElement("#upload_error")
|
||||
expect(view.$(".action-upload")).not.toHaveClass("disabled")
|
||||
|
||||
realMethod = @model.set
|
||||
spyOn(@model, "set").and.callFake (data) ->
|
||||
if data.selectedFile != undefined
|
||||
this.attributes.selectedFile = data.selectedFile
|
||||
this.changed = {}
|
||||
else
|
||||
realMethod.apply(this, arguments)
|
||||
|
||||
event.target = {"files": [inCorrectFile]}
|
||||
view.selectFile(event)
|
||||
expect(view.$el).toContainElement("input[type=file]")
|
||||
expect(view.$el).toContainElement("#upload_error")
|
||||
expect(view.$(".action-upload")).toHaveClass("disabled")
|
||||
|
||||
describe "Uploads", ->
|
||||
beforeEach ->
|
||||
@clock = sinon.useFakeTimers()
|
||||
|
||||
afterEach ->
|
||||
modal_helpers.cancelModalIfShowing()
|
||||
@clock.restore()
|
||||
|
||||
it "can upload correctly", ->
|
||||
requests = AjaxHelpers.requests(this);
|
||||
view = createTestView(this)
|
||||
view.render()
|
||||
view.upload()
|
||||
expect(@model.get("uploading")).toBeTruthy()
|
||||
AjaxHelpers.expectRequest(requests, "POST", "/upload")
|
||||
AjaxHelpers.respondWithJson(requests, { response: "dummy_response"})
|
||||
expect(@model.get("uploading")).toBeFalsy()
|
||||
expect(@model.get("finished")).toBeTruthy()
|
||||
expect(@dialogResponse.pop()).toEqual("dummy_response")
|
||||
|
||||
it "can handle upload errors", ->
|
||||
requests = AjaxHelpers.requests(this);
|
||||
view = createTestView(this)
|
||||
view.render()
|
||||
view.upload()
|
||||
AjaxHelpers.respondWithError(requests)
|
||||
expect(@model.get("title")).toMatch(/error/)
|
||||
expect(view.remove).not.toHaveBeenCalled()
|
||||
|
||||
it "removes itself after two seconds on successful upload", ->
|
||||
requests = AjaxHelpers.requests(this);
|
||||
view = createTestView(this)
|
||||
view.render()
|
||||
view.upload()
|
||||
AjaxHelpers.respondWithJson(requests, { response: "dummy_response"})
|
||||
expect(modal_helpers.isShowingModal(view)).toBeTruthy();
|
||||
@clock.tick(2001)
|
||||
expect(modal_helpers.isShowingModal(view)).toBeFalsy();
|
||||
157
cms/static/coffee/spec/views/upload_spec.js
Normal file
157
cms/static/coffee/spec/views/upload_spec.js
Normal file
@@ -0,0 +1,157 @@
|
||||
define(["sinon", "js/models/uploads", "js/views/uploads", "js/models/chapter",
|
||||
"edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "js/spec_helpers/modal_helpers"],
|
||||
(sinon, FileUpload, UploadDialog, Chapter, AjaxHelpers, modal_helpers) =>
|
||||
|
||||
describe("UploadDialog", function() {
|
||||
const tpl = readFixtures("upload-dialog.underscore");
|
||||
|
||||
beforeEach(function() {
|
||||
let dialogResponse;
|
||||
modal_helpers.installModalTemplates();
|
||||
appendSetFixtures($("<script>", {id: "upload-dialog-tpl", type: "text/template"}).text(tpl));
|
||||
CMS.URL.UPLOAD_ASSET = "/upload";
|
||||
this.model = new FileUpload({
|
||||
mimeTypes: ['application/pdf']
|
||||
});
|
||||
this.dialogResponse = (dialogResponse = []);
|
||||
this.mockFiles = [];});
|
||||
|
||||
afterEach(function() {
|
||||
delete CMS.URL.UPLOAD_ASSET;
|
||||
modal_helpers.cancelModalIfShowing();
|
||||
});
|
||||
|
||||
const createTestView = function(test) {
|
||||
const view = new UploadDialog({
|
||||
model: test.model,
|
||||
url: CMS.URL.UPLOAD_ASSET,
|
||||
onSuccess: response => {
|
||||
return test.dialogResponse.push(response.response);
|
||||
}
|
||||
});
|
||||
spyOn(view, 'remove').and.callThrough();
|
||||
|
||||
// create mock file input, so that we aren't subject to browser restrictions
|
||||
const mockFileInput = jasmine.createSpy('mockFileInput');
|
||||
mockFileInput.files = test.mockFiles;
|
||||
const jqMockFileInput = jasmine.createSpyObj('jqMockFileInput', ['get', 'replaceWith']);
|
||||
jqMockFileInput.get.and.returnValue(mockFileInput);
|
||||
const originalView$ = view.$;
|
||||
spyOn(view, "$").and.callFake(function(selector) {
|
||||
if (selector === "input[type=file]") {
|
||||
return jqMockFileInput;
|
||||
} else {
|
||||
return originalView$.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
this.lastView = view;
|
||||
return view;
|
||||
};
|
||||
|
||||
describe("Basic", function() {
|
||||
it("should render without a file selected", function() {
|
||||
const view = createTestView(this);
|
||||
view.render();
|
||||
expect(view.$el).toContainElement("input[type=file]");
|
||||
expect(view.$(".action-upload")).toHaveClass("disabled");
|
||||
});
|
||||
|
||||
it("should render with a PDF selected", function() {
|
||||
const view = createTestView(this);
|
||||
const file = {name: "fake.pdf", "type": "application/pdf"};
|
||||
this.mockFiles.push(file);
|
||||
this.model.set("selectedFile", file);
|
||||
view.render();
|
||||
expect(view.$el).toContainElement("input[type=file]");
|
||||
expect(view.$el).not.toContainElement("#upload_error");
|
||||
expect(view.$(".action-upload")).not.toHaveClass("disabled");
|
||||
});
|
||||
|
||||
it("should render an error with an invalid file type selected", function() {
|
||||
const view = createTestView(this);
|
||||
const file = {name: "fake.png", "type": "image/png"};
|
||||
this.mockFiles.push(file);
|
||||
this.model.set("selectedFile", file);
|
||||
view.render();
|
||||
expect(view.$el).toContainElement("input[type=file]");
|
||||
expect(view.$el).toContainElement("#upload_error");
|
||||
expect(view.$(".action-upload")).toHaveClass("disabled");
|
||||
});
|
||||
|
||||
it("should render an error with an invalid file type after a correct file type selected", function() {
|
||||
const view = createTestView(this);
|
||||
const correctFile = {name: "fake.pdf", "type": "application/pdf"};
|
||||
const inCorrectFile = {name: "fake.png", "type": "image/png"};
|
||||
const event = {};
|
||||
view.render();
|
||||
|
||||
event.target = {"files": [correctFile]};
|
||||
view.selectFile(event);
|
||||
expect(view.$el).toContainElement("input[type=file]");
|
||||
expect(view.$el).not.toContainElement("#upload_error");
|
||||
expect(view.$(".action-upload")).not.toHaveClass("disabled");
|
||||
|
||||
const realMethod = this.model.set;
|
||||
spyOn(this.model, "set").and.callFake(function(data) {
|
||||
if (data.selectedFile !== undefined) {
|
||||
this.attributes.selectedFile = data.selectedFile;
|
||||
return this.changed = {};
|
||||
} else {
|
||||
return realMethod.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
event.target = {"files": [inCorrectFile]};
|
||||
view.selectFile(event);
|
||||
expect(view.$el).toContainElement("input[type=file]");
|
||||
expect(view.$el).toContainElement("#upload_error");
|
||||
expect(view.$(".action-upload")).toHaveClass("disabled");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Uploads", function() {
|
||||
beforeEach(function() {
|
||||
this.clock = sinon.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
modal_helpers.cancelModalIfShowing();
|
||||
this.clock.restore();
|
||||
});
|
||||
|
||||
it("can upload correctly", function() {
|
||||
const requests = AjaxHelpers.requests(this);
|
||||
const view = createTestView(this);
|
||||
view.render();
|
||||
view.upload();
|
||||
expect(this.model.get("uploading")).toBeTruthy();
|
||||
AjaxHelpers.expectRequest(requests, "POST", "/upload");
|
||||
AjaxHelpers.respondWithJson(requests, { response: "dummy_response"});
|
||||
expect(this.model.get("uploading")).toBeFalsy();
|
||||
expect(this.model.get("finished")).toBeTruthy();
|
||||
expect(this.dialogResponse.pop()).toEqual("dummy_response");
|
||||
});
|
||||
|
||||
it("can handle upload errors", function() {
|
||||
const requests = AjaxHelpers.requests(this);
|
||||
const view = createTestView(this);
|
||||
view.render();
|
||||
view.upload();
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
expect(this.model.get("title")).toMatch(/error/);
|
||||
expect(view.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes itself after two seconds on successful upload", function() {
|
||||
const requests = AjaxHelpers.requests(this);
|
||||
const view = createTestView(this);
|
||||
view.render();
|
||||
view.upload();
|
||||
AjaxHelpers.respondWithJson(requests, { response: "dummy_response"});
|
||||
expect(modal_helpers.isShowingModal(view)).toBeTruthy();
|
||||
this.clock.tick(2001);
|
||||
expect(modal_helpers.isShowingModal(view)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
@@ -1,4 +0,0 @@
|
||||
test_problem_display.js
|
||||
test_problem_generator.js
|
||||
test_problem_grader.js
|
||||
xproblem.js
|
||||
@@ -1,21 +0,0 @@
|
||||
class MinimaxProblemDisplay extends XProblemDisplay
|
||||
|
||||
constructor: (@state, @submission, @evaluation, @container, @submissionField, @parameters={}) ->
|
||||
|
||||
super(@state, @submission, @evaluation, @container, @submissionField, @parameters)
|
||||
|
||||
render: () ->
|
||||
|
||||
createSubmission: () ->
|
||||
|
||||
@newSubmission = {}
|
||||
|
||||
if @submission?
|
||||
for id, value of @submission
|
||||
@newSubmission[id] = value
|
||||
|
||||
getCurrentSubmission: () ->
|
||||
return @newSubmission
|
||||
|
||||
root = exports ? this
|
||||
root.TestProblemDisplay = TestProblemDisplay
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS001: Remove Babel/TypeScript constructor workaround
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS205: Consider reworking code to avoid use of IIFEs
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* DS208: Avoid top-level this
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
class MinimaxProblemDisplay extends XProblemDisplay {
|
||||
|
||||
constructor(state, submission, evaluation, container, submissionField, parameters) {
|
||||
|
||||
{
|
||||
// Hack: trick Babel/TypeScript into allowing this before super.
|
||||
if (false) { super(); }
|
||||
let thisFn = (() => { this; }).toString();
|
||||
let thisName = thisFn.slice(thisFn.indexOf('{') + 1, thisFn.indexOf(';')).trim();
|
||||
eval(`${thisName} = this;`);
|
||||
}
|
||||
this.state = state;
|
||||
this.submission = submission;
|
||||
this.evaluation = evaluation;
|
||||
this.container = container;
|
||||
this.submissionField = submissionField;
|
||||
if (parameters == null) { parameters = {}; }
|
||||
this.parameters = parameters;
|
||||
super(this.state, this.submission, this.evaluation, this.container, this.submissionField, this.parameters);
|
||||
}
|
||||
|
||||
render() {}
|
||||
|
||||
createSubmission() {
|
||||
|
||||
this.newSubmission = {};
|
||||
|
||||
if (this.submission != null) {
|
||||
return (() => {
|
||||
const result = [];
|
||||
for (let id in this.submission) {
|
||||
const value = this.submission[id];
|
||||
result.push(this.newSubmission[id] = value);
|
||||
}
|
||||
return result;
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentSubmission() {
|
||||
return this.newSubmission;
|
||||
}
|
||||
}
|
||||
|
||||
const root = typeof exports !== 'undefined' && exports !== null ? exports : this;
|
||||
root.TestProblemDisplay = TestProblemDisplay;
|
||||
@@ -1,14 +0,0 @@
|
||||
class TestProblemGenerator extends XProblemGenerator
|
||||
|
||||
constructor: (seed, @parameters = {}) ->
|
||||
|
||||
super(seed, @parameters)
|
||||
|
||||
generate: () ->
|
||||
|
||||
@problemState.value = @parameters.value
|
||||
|
||||
return @problemState
|
||||
|
||||
root = exports ? this
|
||||
root.generatorClass = TestProblemGenerator
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS001: Remove Babel/TypeScript constructor workaround
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* DS208: Avoid top-level this
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
class TestProblemGenerator extends XProblemGenerator {
|
||||
|
||||
constructor(seed, parameters) {
|
||||
|
||||
{
|
||||
// Hack: trick Babel/TypeScript into allowing this before super.
|
||||
if (false) { super(); }
|
||||
let thisFn = (() => { this; }).toString();
|
||||
let thisName = thisFn.slice(thisFn.indexOf('{') + 1, thisFn.indexOf(';')).trim();
|
||||
eval(`${thisName} = this;`);
|
||||
}
|
||||
if (parameters == null) { parameters = {}; }
|
||||
this.parameters = parameters;
|
||||
super(seed, this.parameters);
|
||||
}
|
||||
|
||||
generate() {
|
||||
|
||||
this.problemState.value = this.parameters.value;
|
||||
|
||||
return this.problemState;
|
||||
}
|
||||
}
|
||||
|
||||
const root = typeof exports !== 'undefined' && exports !== null ? exports : this;
|
||||
root.generatorClass = TestProblemGenerator;
|
||||
@@ -1,27 +0,0 @@
|
||||
class TestProblemGrader extends XProblemGrader
|
||||
|
||||
constructor: (@submission, @problemState, @parameters={}) ->
|
||||
|
||||
super(@submission, @problemState, @parameters)
|
||||
|
||||
solve: () ->
|
||||
|
||||
@solution = {0: @problemState.value}
|
||||
|
||||
grade: () ->
|
||||
|
||||
if not @solution?
|
||||
@solve()
|
||||
|
||||
allCorrect = true
|
||||
|
||||
for id, value of @solution
|
||||
valueCorrect = if @submission? then (value == @submission[id]) else false
|
||||
@evaluation[id] = valueCorrect
|
||||
if not valueCorrect
|
||||
allCorrect = false
|
||||
|
||||
return allCorrect
|
||||
|
||||
root = exports ? this
|
||||
root.graderClass = TestProblemGrader
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS001: Remove Babel/TypeScript constructor workaround
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* DS208: Avoid top-level this
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
class TestProblemGrader extends XProblemGrader {
|
||||
|
||||
constructor(submission, problemState, parameters) {
|
||||
|
||||
{
|
||||
// Hack: trick Babel/TypeScript into allowing this before super.
|
||||
if (false) { super(); }
|
||||
let thisFn = (() => { this; }).toString();
|
||||
let thisName = thisFn.slice(thisFn.indexOf('{') + 1, thisFn.indexOf(';')).trim();
|
||||
eval(`${thisName} = this;`);
|
||||
}
|
||||
this.submission = submission;
|
||||
this.problemState = problemState;
|
||||
if (parameters == null) { parameters = {}; }
|
||||
this.parameters = parameters;
|
||||
super(this.submission, this.problemState, this.parameters);
|
||||
}
|
||||
|
||||
solve() {
|
||||
|
||||
return this.solution = {0: this.problemState.value};
|
||||
}
|
||||
|
||||
grade() {
|
||||
|
||||
if ((this.solution == null)) {
|
||||
this.solve();
|
||||
}
|
||||
|
||||
let allCorrect = true;
|
||||
|
||||
for (let id in this.solution) {
|
||||
const value = this.solution[id];
|
||||
const valueCorrect = (this.submission != null) ? (value === this.submission[id]) : false;
|
||||
this.evaluation[id] = valueCorrect;
|
||||
if (!valueCorrect) {
|
||||
allCorrect = false;
|
||||
}
|
||||
}
|
||||
|
||||
return allCorrect;
|
||||
}
|
||||
}
|
||||
|
||||
const root = typeof exports !== 'undefined' && exports !== null ? exports : this;
|
||||
root.graderClass = TestProblemGrader;
|
||||
@@ -1,47 +0,0 @@
|
||||
class XProblemGenerator
|
||||
|
||||
constructor: (seed, @parameters={}) ->
|
||||
|
||||
@random = new MersenneTwister(seed)
|
||||
|
||||
@problemState = {}
|
||||
|
||||
generate: () ->
|
||||
|
||||
console.error("Abstract method called: XProblemGenerator.generate")
|
||||
|
||||
class XProblemDisplay
|
||||
|
||||
constructor: (@state, @submission, @evaluation, @container, @submissionField, @parameters={}) ->
|
||||
|
||||
render: () ->
|
||||
|
||||
console.error("Abstract method called: XProblemDisplay.render")
|
||||
|
||||
updateSubmission: () ->
|
||||
|
||||
@submissionField.val(JSON.stringify(@getCurrentSubmission()))
|
||||
|
||||
getCurrentSubmission: () ->
|
||||
console.error("Abstract method called: XProblemDisplay.getCurrentSubmission")
|
||||
|
||||
class XProblemGrader
|
||||
|
||||
constructor: (@submission, @problemState, @parameters={}) ->
|
||||
|
||||
@solution = null
|
||||
@evaluation = {}
|
||||
|
||||
solve: () ->
|
||||
|
||||
console.error("Abstract method called: XProblemGrader.solve")
|
||||
|
||||
grade: () ->
|
||||
|
||||
console.error("Abstract method called: XProblemGrader.grade")
|
||||
|
||||
root = exports ? this
|
||||
|
||||
root.XProblemGenerator = XProblemGenerator
|
||||
root.XProblemDisplay = XProblemDisplay
|
||||
root.XProblemGrader = XProblemGrader
|
||||
78
common/lib/capa/capa/tests/test_files/js/xproblem.js
Normal file
78
common/lib/capa/capa/tests/test_files/js/xproblem.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* DS208: Avoid top-level this
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
class XProblemGenerator {
|
||||
|
||||
constructor(seed, parameters) {
|
||||
|
||||
if (parameters == null) { parameters = {}; }
|
||||
this.parameters = parameters;
|
||||
this.random = new MersenneTwister(seed);
|
||||
|
||||
this.problemState = {};
|
||||
}
|
||||
|
||||
generate() {
|
||||
|
||||
console.error("Abstract method called: XProblemGenerator.generate");
|
||||
}
|
||||
}
|
||||
|
||||
class XProblemDisplay {
|
||||
|
||||
constructor(state, submission, evaluation, container, submissionField, parameters) {
|
||||
this.state = state;
|
||||
this.submission = submission;
|
||||
this.evaluation = evaluation;
|
||||
this.container = container;
|
||||
this.submissionField = submissionField;
|
||||
if (parameters == null) { parameters = {}; }
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
console.error("Abstract method called: XProblemDisplay.render");
|
||||
}
|
||||
|
||||
updateSubmission() {
|
||||
|
||||
this.submissionField.val(JSON.stringify(this.getCurrentSubmission()));
|
||||
}
|
||||
|
||||
getCurrentSubmission() {
|
||||
console.error("Abstract method called: XProblemDisplay.getCurrentSubmission");
|
||||
}
|
||||
}
|
||||
|
||||
class XProblemGrader {
|
||||
|
||||
constructor(submission, problemState, parameters) {
|
||||
|
||||
this.submission = submission;
|
||||
this.problemState = problemState;
|
||||
if (parameters == null) { parameters = {}; }
|
||||
this.parameters = parameters;
|
||||
this.solution = null;
|
||||
this.evaluation = {};
|
||||
}
|
||||
|
||||
solve() {
|
||||
|
||||
console.error("Abstract method called: XProblemGrader.solve");
|
||||
}
|
||||
|
||||
grade() {
|
||||
|
||||
console.error("Abstract method called: XProblemGrader.grade");
|
||||
}
|
||||
}
|
||||
|
||||
const root = typeof exports !== 'undefined' && exports !== null ? exports : this;
|
||||
|
||||
root.XProblemGenerator = XProblemGenerator;
|
||||
root.XProblemDisplay = XProblemDisplay;
|
||||
root.XProblemGrader = XProblemGrader;
|
||||
10
common/lib/xmodule/xmodule/js/spec/.gitignore
vendored
10
common/lib/xmodule/xmodule/js/spec/.gitignore
vendored
@@ -1,10 +0,0 @@
|
||||
*.js
|
||||
|
||||
# Tests for video are written in pure JavaScript.
|
||||
!video/*.js
|
||||
|
||||
# Tests for Time are written in pure JavaScript.
|
||||
!time_spec.js
|
||||
!collapsible_spec.js
|
||||
!xmodule_spec.js
|
||||
!sequence/display_spec.js
|
||||
@@ -1,9 +0,0 @@
|
||||
describe 'Annotatable', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'annotatable.html'
|
||||
describe 'constructor', ->
|
||||
el = $('.xblock-student_view.xmodule_AnnotatableModule')
|
||||
beforeEach ->
|
||||
@annotatable = new Annotatable(el)
|
||||
it 'works', ->
|
||||
expect(1).toBe(1)
|
||||
@@ -0,0 +1,10 @@
|
||||
describe('Annotatable', function() {
|
||||
beforeEach(() => loadFixtures('annotatable.html'));
|
||||
describe('constructor', function() {
|
||||
const el = $('.xblock-student_view.xmodule_AnnotatableModule');
|
||||
beforeEach(function() {
|
||||
this.annotatable = new Annotatable(el);
|
||||
});
|
||||
it('works', () => expect(1).toBe(1));
|
||||
});
|
||||
});
|
||||
@@ -1,846 +0,0 @@
|
||||
describe 'Problem', ->
|
||||
problem_content_default = readFixtures('problem_content.html')
|
||||
|
||||
beforeEach ->
|
||||
# Stub MathJax
|
||||
window.MathJax =
|
||||
Hub: jasmine.createSpyObj('MathJax.Hub', ['getAllJax', 'Queue'])
|
||||
Callback: jasmine.createSpyObj('MathJax.Callback', ['After'])
|
||||
@stubbedJax = root: jasmine.createSpyObj('jax.root', ['toMathML'])
|
||||
MathJax.Hub.getAllJax.and.returnValue [@stubbedJax]
|
||||
window.update_schematics = ->
|
||||
spyOn SR, 'readText'
|
||||
spyOn SR, 'readTexts'
|
||||
|
||||
# Load this function from spec/helper.coffee
|
||||
# Note that if your test fails with a message like:
|
||||
# 'External request attempted for blah, which is not defined.'
|
||||
# this msg is coming from the stubRequests function else clause.
|
||||
jasmine.stubRequests()
|
||||
|
||||
loadFixtures 'problem.html'
|
||||
|
||||
spyOn Logger, 'log'
|
||||
spyOn($.fn, 'load').and.callFake (url, callback) ->
|
||||
$(@).html readFixtures('problem_content.html')
|
||||
callback()
|
||||
|
||||
describe 'constructor', ->
|
||||
|
||||
it 'set the element from html', ->
|
||||
@problem999 = new Problem ("
|
||||
<section class='xblock xblock-student_view xmodule_display xmodule_CapaModule' data-type='Problem'>
|
||||
<section id='problem_999'
|
||||
class='problems-wrapper'
|
||||
data-problem-id='i4x://edX/999/problem/Quiz'
|
||||
data-url='/problem/quiz/'>
|
||||
</section>
|
||||
</section>
|
||||
")
|
||||
expect(@problem999.element_id).toBe 'problem_999'
|
||||
|
||||
it 'set the element from loadFixtures', ->
|
||||
@problem1 = new Problem($('.xblock-student_view'))
|
||||
expect(@problem1.element_id).toBe 'problem_1'
|
||||
|
||||
describe 'bind', ->
|
||||
beforeEach ->
|
||||
spyOn window, 'update_schematics'
|
||||
MathJax.Hub.getAllJax.and.returnValue [@stubbedJax]
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
|
||||
it 'set mathjax typeset', ->
|
||||
expect(MathJax.Hub.Queue).toHaveBeenCalled()
|
||||
|
||||
it 'update schematics', ->
|
||||
expect(window.update_schematics).toHaveBeenCalled()
|
||||
|
||||
it 'bind answer refresh on button click', ->
|
||||
expect($('div.action button')).toHandleWith 'click', @problem.refreshAnswers
|
||||
|
||||
it 'bind the submit button', ->
|
||||
expect($('.action .submit')).toHandleWith 'click', @problem.submit_fd
|
||||
|
||||
it 'bind the reset button', ->
|
||||
expect($('div.action button.reset')).toHandleWith 'click', @problem.reset
|
||||
|
||||
it 'bind the show button', ->
|
||||
expect($('.action .show')).toHandleWith 'click', @problem.show
|
||||
|
||||
it 'bind the save button', ->
|
||||
expect($('div.action button.save')).toHandleWith 'click', @problem.save
|
||||
|
||||
it 'bind the math input', ->
|
||||
expect($('input.math')).toHandleWith 'keyup', @problem.refreshMath
|
||||
|
||||
describe 'bind_with_custom_input_id', ->
|
||||
beforeEach ->
|
||||
spyOn window, 'update_schematics'
|
||||
MathJax.Hub.getAllJax.and.returnValue [@stubbedJax]
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
$(@).html readFixtures('problem_content_1240.html')
|
||||
|
||||
it 'bind the submit button', ->
|
||||
expect($('.action .submit')).toHandleWith 'click', @problem.submit_fd
|
||||
|
||||
it 'bind the show button', ->
|
||||
expect($('div.action button.show')).toHandleWith 'click', @problem.show
|
||||
|
||||
|
||||
describe 'renderProgressState', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
|
||||
testProgessData = (problem, score, total_possible, attempts, graded, expected_progress_after_render) ->
|
||||
problem.el.data('problem-score', score);
|
||||
problem.el.data('problem-total-possible', total_possible);
|
||||
problem.el.data('attempts-used', attempts);
|
||||
problem.el.data('graded', graded)
|
||||
expect(problem.$('.problem-progress').html()).toEqual ""
|
||||
problem.renderProgressState()
|
||||
expect(problem.$('.problem-progress').html()).toEqual expected_progress_after_render
|
||||
|
||||
describe 'with a status of "none"', ->
|
||||
it 'reports the number of points possible and graded', ->
|
||||
testProgessData(@problem, 0, 1, 0, "True", "1 point possible (graded)")
|
||||
|
||||
it 'displays the number of points possible when rendering happens with the content', ->
|
||||
testProgessData(@problem, 0, 2, 0, "True", "2 points possible (graded)")
|
||||
|
||||
it 'reports the number of points possible and ungraded', ->
|
||||
testProgessData(@problem, 0, 1, 0, "False", "1 point possible (ungraded)")
|
||||
|
||||
it 'displays ungraded if number of points possible is 0', ->
|
||||
testProgessData(@problem, 0, 0, 0, "False", "0 points possible (ungraded)")
|
||||
|
||||
it 'displays ungraded if number of points possible is 0, even if graded value is True', ->
|
||||
testProgessData(@problem, 0, 0, 0, "True", "0 points possible (ungraded)")
|
||||
|
||||
it 'reports the correct score with status none and >0 attempts', ->
|
||||
testProgessData(@problem, 0, 1, 1, "True", "0/1 point (graded)")
|
||||
|
||||
it 'reports the correct score with >1 weight, status none, and >0 attempts', ->
|
||||
testProgessData(@problem, 0, 2, 2, "True", "0/2 points (graded)")
|
||||
|
||||
describe 'with any other valid status', ->
|
||||
|
||||
it 'reports the current score', ->
|
||||
testProgessData(@problem, 1, 1, 1, "True", "1/1 point (graded)")
|
||||
|
||||
it 'shows current score when rendering happens with the content', ->
|
||||
testProgessData(@problem, 2, 2, 1, "True", "2/2 points (graded)")
|
||||
|
||||
it 'reports the current score even if problem is ungraded', ->
|
||||
testProgessData(@problem, 1, 1, 1, "False", "1/1 point (ungraded)")
|
||||
|
||||
describe 'with valid status and string containing an integer like "0" for detail', ->
|
||||
# These tests are to address a failure specific to Chrome 51 and 52 +
|
||||
it 'shows 0 points possible for the detail', ->
|
||||
testProgessData(@problem, 0, 0, 1, "False", "0 points possible (ungraded)")
|
||||
|
||||
describe 'with a score of null (show_correctness == false)', ->
|
||||
it 'reports the number of points possible and graded, results hidden', ->
|
||||
testProgessData(@problem, null, 1, 0, "True", "1 point possible (graded, results hidden)")
|
||||
|
||||
it 'reports the number of points possible (plural) and graded, results hidden', ->
|
||||
testProgessData(@problem, null, 2, 0, "True", "2 points possible (graded, results hidden)")
|
||||
|
||||
it 'reports the number of points possible and ungraded, results hidden', ->
|
||||
testProgessData(@problem, null, 1, 0, "False", "1 point possible (ungraded, results hidden)")
|
||||
|
||||
it 'displays ungraded if number of points possible is 0, results hidden', ->
|
||||
testProgessData(@problem, null, 0, 0, "False", "0 points possible (ungraded, results hidden)")
|
||||
|
||||
it 'displays ungraded if number of points possible is 0, even if graded value is True, results hidden', ->
|
||||
testProgessData(@problem, null, 0, 0, "True", "0 points possible (ungraded, results hidden)")
|
||||
|
||||
it 'reports the correct score with status none and >0 attempts, results hidden', ->
|
||||
testProgessData(@problem, null, 1, 1, "True", "1 point possible (graded, results hidden)")
|
||||
|
||||
it 'reports the correct score with >1 weight, status none, and >0 attempts, results hidden', ->
|
||||
testProgessData(@problem, null, 2, 2, "True", "2 points possible (graded, results hidden)")
|
||||
|
||||
describe 'render', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
@bind = @problem.bind
|
||||
spyOn @problem, 'bind'
|
||||
|
||||
describe 'with content given', ->
|
||||
beforeEach ->
|
||||
@problem.render 'Hello World'
|
||||
|
||||
it 'render the content', ->
|
||||
expect(@problem.el.html()).toEqual 'Hello World'
|
||||
|
||||
it 're-bind the content', ->
|
||||
expect(@problem.bind).toHaveBeenCalled()
|
||||
|
||||
describe 'with no content given', ->
|
||||
beforeEach ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, callback) ->
|
||||
callback html: "Hello World"
|
||||
@problem.render()
|
||||
|
||||
it 'load the content via ajax', ->
|
||||
expect(@problem.el.html()).toEqual 'Hello World'
|
||||
|
||||
it 're-bind the content', ->
|
||||
expect(@problem.bind).toHaveBeenCalled()
|
||||
|
||||
describe 'submit_fd', ->
|
||||
beforeEach ->
|
||||
# Insert an input of type file outside of the problem.
|
||||
$('.xblock-student_view').after('<input type="file" />')
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
spyOn(@problem, 'submit')
|
||||
|
||||
it 'submit method is called if input of type file is not in problem', ->
|
||||
@problem.submit_fd()
|
||||
expect(@problem.submit).toHaveBeenCalled()
|
||||
|
||||
describe 'submit', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
@problem.answers = 'foo=1&bar=2'
|
||||
|
||||
it 'log the problem_check event', ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
done: (callable) -> callable()
|
||||
@problem.submit()
|
||||
expect(Logger.log).toHaveBeenCalledWith 'problem_check', 'foo=1&bar=2'
|
||||
|
||||
it 'log the problem_graded event, after the problem is done grading.', ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
response =
|
||||
success: 'correct'
|
||||
contents: 'mock grader response'
|
||||
callback(response)
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
done: (callable) -> callable()
|
||||
@problem.submit()
|
||||
expect(Logger.log).toHaveBeenCalledWith 'problem_graded', ['foo=1&bar=2', 'mock grader response'], @problem.id
|
||||
|
||||
it 'submit the answer for submit', ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
done: (callable) -> callable()
|
||||
@problem.submit()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_check',
|
||||
'foo=1&bar=2', jasmine.any(Function)
|
||||
|
||||
describe 'when the response is correct', ->
|
||||
it 'call render with returned content', ->
|
||||
contents = '<div class="wrapper-problem-response" aria-label="Question 1"><p>Correct<span class="status">excellent</span></p></div>' +
|
||||
'<div class="wrapper-problem-response" aria-label="Question 2"><p>Yep<span class="status">correct</span></p></div>'
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
callback(success: 'correct', contents: contents)
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
done: (callable) -> callable()
|
||||
@problem.submit()
|
||||
expect(@problem.el).toHaveHtml contents
|
||||
expect(window.SR.readTexts).toHaveBeenCalledWith ['Question 1: excellent', 'Question 2: correct']
|
||||
|
||||
describe 'when the response is incorrect', ->
|
||||
it 'call render with returned content', ->
|
||||
contents = '<p>Incorrect<span class="status">no, try again</span></p>'
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
callback(success: 'incorrect', contents: contents)
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
done: (callable) -> callable()
|
||||
@problem.submit()
|
||||
expect(@problem.el).toHaveHtml contents
|
||||
expect(window.SR.readTexts).toHaveBeenCalledWith ['no, try again']
|
||||
|
||||
it 'tests if the submit button is disabled while submitting and the text changes on the button', ->
|
||||
self = this
|
||||
curr_html = @problem.el.html()
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
# At this point enableButtons should have been called, making the submit button disabled with text 'submitting'
|
||||
expect(self.problem.submitButton).toHaveAttr('disabled');
|
||||
expect(self.problem.submitButtonLabel.text()).toBe('Submitting');
|
||||
callback
|
||||
success: 'incorrect' # does not matter if correct or incorrect here
|
||||
contents: curr_html
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
done: (callable) -> callable()
|
||||
# Make sure the submit button is enabled before submitting
|
||||
$('#input_example_1').val('test').trigger('input')
|
||||
expect(@problem.submitButton).not.toHaveAttr('disabled')
|
||||
@problem.submit()
|
||||
# After submit, the button should not be disabled and should have text as 'Submit'
|
||||
expect(@problem.submitButtonLabel.text()).toBe('Submit')
|
||||
expect(@problem.submitButton).not.toHaveAttr('disabled')
|
||||
|
||||
describe 'submit button on problems', ->
|
||||
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
@submitDisabled = (disabled) =>
|
||||
if disabled
|
||||
expect(@problem.submitButton).toHaveAttr('disabled')
|
||||
else
|
||||
expect(@problem.submitButton).not.toHaveAttr('disabled')
|
||||
|
||||
describe 'some basic tests for submit button', ->
|
||||
it 'should become enabled after a value is entered into the text box', ->
|
||||
$('#input_example_1').val('test').trigger('input')
|
||||
@submitDisabled false
|
||||
$('#input_example_1').val('').trigger('input')
|
||||
@submitDisabled true
|
||||
|
||||
describe 'some advanced tests for submit button', ->
|
||||
radioButtonProblemHtml = readFixtures('radiobutton_problem.html')
|
||||
checkboxProblemHtml = readFixtures('checkbox_problem.html')
|
||||
|
||||
it 'should become enabled after a checkbox is checked', ->
|
||||
$('#input_example_1').replaceWith(checkboxProblemHtml)
|
||||
@problem.submitAnswersAndSubmitButton true
|
||||
@submitDisabled true
|
||||
$('#input_1_1_1').click()
|
||||
@submitDisabled false
|
||||
$('#input_1_1_1').click()
|
||||
@submitDisabled true
|
||||
|
||||
it 'should become enabled after a radiobutton is checked', ->
|
||||
$('#input_example_1').replaceWith(radioButtonProblemHtml)
|
||||
@problem.submitAnswersAndSubmitButton true
|
||||
@submitDisabled true
|
||||
$('#input_1_1_1').attr('checked', true).trigger('click')
|
||||
@submitDisabled false
|
||||
$('#input_1_1_1').attr('checked', false).trigger('click')
|
||||
@submitDisabled true
|
||||
|
||||
it 'should become enabled after a value is selected in a selector', ->
|
||||
html = '''
|
||||
<div id="problem_sel">
|
||||
<select>
|
||||
<option value="val0">Select an option</option>
|
||||
<option value="val1">1</option>
|
||||
<option value="val2">2</option>
|
||||
</select>
|
||||
</div>
|
||||
'''
|
||||
$('#input_example_1').replaceWith(html)
|
||||
@problem.submitAnswersAndSubmitButton true
|
||||
@submitDisabled true
|
||||
$("#problem_sel select").val("val2").trigger('change')
|
||||
@submitDisabled false
|
||||
$("#problem_sel select").val("val0").trigger('change')
|
||||
@submitDisabled true
|
||||
|
||||
it 'should become enabled after a radiobutton is checked and a value is entered into the text box', ->
|
||||
$(radioButtonProblemHtml).insertAfter('#input_example_1')
|
||||
@problem.submitAnswersAndSubmitButton true
|
||||
@submitDisabled true
|
||||
$('#input_1_1_1').attr('checked', true).trigger('click')
|
||||
@submitDisabled true
|
||||
$('#input_example_1').val('111').trigger('input')
|
||||
@submitDisabled false
|
||||
$('#input_1_1_1').attr('checked', false).trigger('click')
|
||||
@submitDisabled true
|
||||
|
||||
it 'should become enabled if there are only hidden input fields', ->
|
||||
html = '''
|
||||
<input type="text" name="test" id="test" aria-describedby="answer_test" value="" style="display:none;">
|
||||
'''
|
||||
$('#input_example_1').replaceWith(html)
|
||||
@problem.submitAnswersAndSubmitButton true
|
||||
@submitDisabled false
|
||||
|
||||
describe 'reset', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
|
||||
it 'log the problem_reset event', ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
@problem.answers = 'foo=1&bar=2'
|
||||
@problem.reset()
|
||||
expect(Logger.log).toHaveBeenCalledWith 'problem_reset', 'foo=1&bar=2'
|
||||
|
||||
it 'POST to the problem reset page', ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
@problem.reset()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_reset',
|
||||
{ id: 'i4x://edX/101/problem/Problem1' }, jasmine.any(Function)
|
||||
|
||||
it 'render the returned content', ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
callback html: "Reset", success: true
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
@problem.reset()
|
||||
expect(@problem.el.html()).toEqual 'Reset'
|
||||
|
||||
it 'sends a message to the window SR element', ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
callback html: "Reset", success: true
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
@problem.reset()
|
||||
expect(window.SR.readText).toHaveBeenCalledWith 'This problem has been reset.'
|
||||
|
||||
it 'shows a notification on error', ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
callback msg: "Error on reset.", success: false
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
@problem.reset()
|
||||
expect($('.notification-gentle-alert .notification-message').text()).toEqual("Error on reset.")
|
||||
|
||||
it 'tests that reset does not enable submit or modify the text while resetting', ->
|
||||
self = this
|
||||
curr_html = @problem.el.html()
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
# enableButtons should have been called at this point to set them to all disabled
|
||||
expect(self.problem.submitButton).toHaveAttr('disabled')
|
||||
expect(self.problem.submitButtonLabel.text()).toBe('Submit')
|
||||
callback(success: 'correct', html: curr_html)
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
# Submit should be disabled
|
||||
expect(@problem.submitButton).toHaveAttr('disabled')
|
||||
@problem.reset()
|
||||
# Submit should remain disabled
|
||||
expect(self.problem.submitButton).toHaveAttr('disabled')
|
||||
expect(self.problem.submitButtonLabel.text()).toBe('Submit')
|
||||
|
||||
describe 'show', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
@problem.el.prepend '<div id="answer_1_1" /><div id="answer_1_2" />'
|
||||
|
||||
describe 'when the answer has not yet shown', ->
|
||||
beforeEach ->
|
||||
expect(@problem.el.find('.show').attr('disabled')).not.toEqual('disabled')
|
||||
|
||||
it 'log the problem_show event', ->
|
||||
@problem.show()
|
||||
expect(Logger.log).toHaveBeenCalledWith 'problem_show',
|
||||
problem: 'i4x://edX/101/problem/Problem1'
|
||||
|
||||
it 'fetch the answers', ->
|
||||
spyOn $, 'postWithPrefix'
|
||||
@problem.show()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_show',
|
||||
jasmine.any(Function)
|
||||
|
||||
it 'show the answers', ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, callback) ->
|
||||
callback answers: '1_1': 'One', '1_2': 'Two'
|
||||
@problem.show()
|
||||
expect($('#answer_1_1')).toHaveHtml 'One'
|
||||
expect($('#answer_1_2')).toHaveHtml 'Two'
|
||||
|
||||
it 'disables the show answer button', ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, callback) -> callback(answers: {})
|
||||
@problem.show()
|
||||
expect(@problem.el.find('.show').attr('disabled')).toEqual('disabled')
|
||||
|
||||
describe 'radio text question', ->
|
||||
radio_text_xml='''
|
||||
<section class="problem">
|
||||
<div><p></p><span><section id="choicetextinput_1_2_1" class="choicetextinput">
|
||||
|
||||
<form class="choicetextgroup capa_inputtype" id="inputtype_1_2_1">
|
||||
<div class="indicator-container">
|
||||
<span class="unanswered" style="display:inline-block;" id="status_1_2_1"></span>
|
||||
</div>
|
||||
<fieldset>
|
||||
<section id="forinput1_2_1_choiceinput_0bc">
|
||||
<input class="ctinput" type="radio" name="choiceinput_1_2_1" id="1_2_1_choiceinput_0bc" value="choiceinput_0"">
|
||||
<input class="ctinput" type="text" name="choiceinput_0_textinput_0" id="1_2_1_choiceinput_0_textinput_0" value=" ">
|
||||
<p id="answer_1_2_1_choiceinput_0bc" class="answer"></p>
|
||||
</>
|
||||
<section id="forinput1_2_1_choiceinput_1bc">
|
||||
<input class="ctinput" type="radio" name="choiceinput_1_2_1" id="1_2_1_choiceinput_1bc" value="choiceinput_1" >
|
||||
<input class="ctinput" type="text" name="choiceinput_1_textinput_0" id="1_2_1_choiceinput_1_textinput_0" value=" " >
|
||||
<p id="answer_1_2_1_choiceinput_1bc" class="answer"></p>
|
||||
</section>
|
||||
<section id="forinput1_2_1_choiceinput_2bc">
|
||||
<input class="ctinput" type="radio" name="choiceinput_1_2_1" id="1_2_1_choiceinput_2bc" value="choiceinput_2" >
|
||||
<input class="ctinput" type="text" name="choiceinput_2_textinput_0" id="1_2_1_choiceinput_2_textinput_0" value=" " >
|
||||
<p id="answer_1_2_1_choiceinput_2bc" class="answer"></p>
|
||||
</section></fieldset><input class="choicetextvalue" type="hidden" name="input_1_2_1" id="input_1_2_1"></form>
|
||||
</section></span></div>
|
||||
</section>
|
||||
'''
|
||||
beforeEach ->
|
||||
# Append a radiotextresponse problem to the problem, so we can check it's javascript functionality
|
||||
@problem.el.prepend(radio_text_xml)
|
||||
|
||||
it 'sets the correct class on the section for the correct choice', ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, callback) ->
|
||||
callback answers: "1_2_1": ["1_2_1_choiceinput_0bc"], "1_2_1_choiceinput_0bc": "3"
|
||||
@problem.show()
|
||||
|
||||
expect($('#forinput1_2_1_choiceinput_0bc').attr('class')).toEqual(
|
||||
'choicetextgroup_show_correct')
|
||||
expect($('#answer_1_2_1_choiceinput_0bc').text()).toEqual('3')
|
||||
expect($('#answer_1_2_1_choiceinput_1bc').text()).toEqual('')
|
||||
expect($('#answer_1_2_1_choiceinput_2bc').text()).toEqual('')
|
||||
|
||||
it 'Should not disable input fields', ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, callback) ->
|
||||
callback answers: "1_2_1": ["1_2_1_choiceinput_0bc"], "1_2_1_choiceinput_0bc": "3"
|
||||
@problem.show()
|
||||
expect($('input#1_2_1_choiceinput_0bc').attr('disabled')).not.toEqual('disabled')
|
||||
expect($('input#1_2_1_choiceinput_1bc').attr('disabled')).not.toEqual('disabled')
|
||||
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 = readFixtures('imageinput.underscore')
|
||||
|
||||
DEFAULTS =
|
||||
id: '12345'
|
||||
width: '300'
|
||||
height: '400'
|
||||
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
@problem.el.prepend _.template(imageinput_html)(DEFAULTS)
|
||||
|
||||
assertAnswer = (problem, data) =>
|
||||
stubRequest(data)
|
||||
problem.show()
|
||||
|
||||
$.each data['answers'], (id, answer) =>
|
||||
img = getImage(answer)
|
||||
el = $('#inputtype_' + id)
|
||||
expect(img).toImageDiffEqual(el.find('canvas')[0])
|
||||
|
||||
stubRequest = (data) =>
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, callback) ->
|
||||
callback 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 = abs(points[3] - points[1])
|
||||
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]? and value
|
||||
|
||||
return canvas
|
||||
|
||||
it 'rectangle is drawn correctly', ->
|
||||
assertAnswer(@problem, {
|
||||
'answers':
|
||||
'12345':
|
||||
'rectangle': '(10,10)-(30,30)',
|
||||
'regions': null
|
||||
})
|
||||
|
||||
it 'region is drawn correctly', ->
|
||||
assertAnswer(@problem, {
|
||||
'answers':
|
||||
'12345':
|
||||
'rectangle': null,
|
||||
'regions': '[[10,10],[30,30],[70,30],[20,30]]'
|
||||
})
|
||||
|
||||
it 'mixed shapes are drawn correctly', ->
|
||||
assertAnswer(@problem, {
|
||||
'answers':'12345':
|
||||
'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]]
|
||||
]'''
|
||||
})
|
||||
|
||||
it 'multiple image inputs draw answers on separate canvases', ->
|
||||
data =
|
||||
id: '67890'
|
||||
width: '400'
|
||||
height: '300'
|
||||
|
||||
@problem.el.prepend _.template(imageinput_html)(data)
|
||||
assertAnswer(@problem, {
|
||||
'answers':
|
||||
'12345':
|
||||
'rectangle': null,
|
||||
'regions': '[[10,10],[30,30],[70,30],[20,30]]'
|
||||
'67890':
|
||||
'rectangle': '(10,10)-(30,30)',
|
||||
'regions': null
|
||||
})
|
||||
|
||||
it 'dictionary with answers doesn\'t contain answer for current id', ->
|
||||
spyOn console, 'log'
|
||||
stubRequest({'answers':{}})
|
||||
@problem.show()
|
||||
el = $('#inputtype_12345')
|
||||
expect(el.find('canvas')).not.toExist()
|
||||
expect(console.log).toHaveBeenCalledWith('Answer is absent for image input with id=12345')
|
||||
|
||||
describe 'save', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
@problem.answers = 'foo=1&bar=2'
|
||||
|
||||
it 'log the problem_save event', ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
@problem.save()
|
||||
expect(Logger.log).toHaveBeenCalledWith 'problem_save', 'foo=1&bar=2'
|
||||
|
||||
it 'POST to save problem', ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
@problem.save()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_save',
|
||||
'foo=1&bar=2', jasmine.any(Function)
|
||||
|
||||
it 'tests that save does not enable the submit button or change the text when submit is originally disabled', ->
|
||||
self = this
|
||||
curr_html = @problem.el.html()
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
# enableButtons should have been called at this point and the submit button should be unaffected
|
||||
expect(self.problem.submitButton).toHaveAttr('disabled')
|
||||
expect(self.problem.submitButtonLabel.text()).toBe('Submit')
|
||||
callback(success: 'correct', html: curr_html)
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
# Expect submit to be disabled and labeled properly at the start
|
||||
expect(@problem.submitButton).toHaveAttr('disabled')
|
||||
expect(@problem.submitButtonLabel.text()).toBe('Submit')
|
||||
@problem.save()
|
||||
# Submit button should have the same state after save has completed
|
||||
expect(@problem.submitButton).toHaveAttr('disabled')
|
||||
expect(@problem.submitButtonLabel.text()).toBe('Submit')
|
||||
|
||||
it 'tests that save does not disable the submit button or change the text when submit is originally enabled', ->
|
||||
self = this
|
||||
curr_html = @problem.el.html()
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, answers, callback) ->
|
||||
# enableButtons should have been called at this point, and the submit button should be disabled while submitting
|
||||
expect(self.problem.submitButton).toHaveAttr('disabled')
|
||||
expect(self.problem.submitButtonLabel.text()).toBe('Submit')
|
||||
callback(success: 'correct', html: curr_html)
|
||||
promise =
|
||||
always: (callable) -> callable()
|
||||
# Expect submit to be enabled and labeled properly at the start after adding an input
|
||||
$('#input_example_1').val('test').trigger('input')
|
||||
expect(@problem.submitButton).not.toHaveAttr('disabled')
|
||||
expect(@problem.submitButtonLabel.text()).toBe('Submit')
|
||||
@problem.save()
|
||||
# Submit button should have the same state after save has completed
|
||||
expect(@problem.submitButton).not.toHaveAttr('disabled')
|
||||
expect(@problem.submitButtonLabel.text()).toBe('Submit')
|
||||
|
||||
describe 'refreshMath', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
$('#input_example_1').val 'E=mc^2'
|
||||
@problem.refreshMath target: $('#input_example_1').get(0)
|
||||
|
||||
it 'should queue the conversion and MathML element update', ->
|
||||
expect(MathJax.Hub.Queue).toHaveBeenCalledWith ['Text', @stubbedJax, 'E=mc^2'],
|
||||
[@problem.updateMathML, @stubbedJax, $('#input_example_1').get(0)]
|
||||
|
||||
describe 'updateMathML', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
@stubbedJax.root.toMathML.and.returnValue '<MathML>'
|
||||
|
||||
describe 'when there is no exception', ->
|
||||
beforeEach ->
|
||||
@problem.updateMathML @stubbedJax, $('#input_example_1').get(0)
|
||||
|
||||
it 'convert jax to MathML', ->
|
||||
expect($('#input_example_1_dynamath')).toHaveValue '<MathML>'
|
||||
|
||||
describe 'when there is an exception', ->
|
||||
beforeEach ->
|
||||
error = new Error()
|
||||
error.restart = true
|
||||
@stubbedJax.root.toMathML.and.throwError error
|
||||
@problem.updateMathML @stubbedJax, $('#input_example_1').get(0)
|
||||
|
||||
it 'should queue up the exception', ->
|
||||
expect(MathJax.Callback.After).toHaveBeenCalledWith [@problem.refreshMath, @stubbedJax], true
|
||||
|
||||
describe 'refreshAnswers', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
@problem.el.html '''
|
||||
<textarea class="CodeMirror" />
|
||||
<input id="input_1_1" name="input_1_1" class="schematic" value="one" />
|
||||
<input id="input_1_2" name="input_1_2" value="two" />
|
||||
<input id="input_bogus_3" name="input_bogus_3" value="three" />
|
||||
'''
|
||||
@stubSchematic = { update_value: jasmine.createSpy('schematic') }
|
||||
@stubCodeMirror = { save: jasmine.createSpy('CodeMirror') }
|
||||
$('input.schematic').get(0).schematic = @stubSchematic
|
||||
$('textarea.CodeMirror').get(0).CodeMirror = @stubCodeMirror
|
||||
|
||||
it 'update each schematic', ->
|
||||
@problem.refreshAnswers()
|
||||
expect(@stubSchematic.update_value).toHaveBeenCalled()
|
||||
|
||||
it 'update each code block', ->
|
||||
@problem.refreshAnswers()
|
||||
expect(@stubCodeMirror.save).toHaveBeenCalled()
|
||||
|
||||
describe 'multiple JsInput in single problem', ->
|
||||
jsinput_html = readFixtures('jsinput_problem.html')
|
||||
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
@problem.render(jsinput_html)
|
||||
|
||||
it 'submit_save_waitfor should return false', ->
|
||||
$(@problem.inputs[0]).data('waitfor', ->)
|
||||
expect(@problem.submit_save_waitfor()).toEqual(false)
|
||||
|
||||
describe 'Submitting an xqueue-graded problem', ->
|
||||
matlabinput_html = readFixtures('matlabinput_problem.html')
|
||||
|
||||
beforeEach ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, callback) ->
|
||||
callback html: matlabinput_html
|
||||
jasmine.clock().install()
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
spyOn(@problem, 'poll').and.callThrough()
|
||||
@problem.render(matlabinput_html)
|
||||
|
||||
afterEach ->
|
||||
jasmine.clock().uninstall()
|
||||
|
||||
it 'check that we stop polling after a fixed amount of time', ->
|
||||
expect(@problem.poll).not.toHaveBeenCalled()
|
||||
jasmine.clock().tick(1)
|
||||
time_steps = [1000, 2000, 4000, 8000, 16000, 32000]
|
||||
num_calls = 1
|
||||
for time_step in time_steps
|
||||
do (time_step) =>
|
||||
jasmine.clock().tick(time_step)
|
||||
expect(@problem.poll.calls.count()).toEqual(num_calls)
|
||||
num_calls += 1
|
||||
|
||||
# jump the next step and verify that we are not still continuing to poll
|
||||
jasmine.clock().tick(64000)
|
||||
expect(@problem.poll.calls.count()).toEqual(6)
|
||||
|
||||
expect($('.notification-gentle-alert .notification-message').text()).toEqual("The grading process is still running. Refresh the page to see updates.")
|
||||
|
||||
describe 'codeinput problem', ->
|
||||
codeinputProblemHtml = readFixtures('codeinput_problem.html')
|
||||
|
||||
beforeEach ->
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, callback) ->
|
||||
callback html: codeinputProblemHtml
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
@problem.render(codeinputProblemHtml)
|
||||
|
||||
it 'has rendered with correct a11y info', ->
|
||||
CodeMirrorTextArea = $('textarea')[1]
|
||||
CodeMirrorTextAreaId = 'cm-textarea-101'
|
||||
|
||||
# verify that question label has correct `for` attribute value
|
||||
expect($('.problem-group-label').attr('for')).toEqual(CodeMirrorTextAreaId)
|
||||
|
||||
# verify that codemirror textarea has correct `id` attribute value
|
||||
expect($(CodeMirrorTextArea).attr('id')).toEqual(CodeMirrorTextAreaId)
|
||||
|
||||
# verify that codemirror textarea has correct `aria-describedby` attribute value
|
||||
expect($(CodeMirrorTextArea).attr('aria-describedby')).toEqual('cm-editor-exit-message-101 status_101')
|
||||
|
||||
|
||||
describe 'show answer button', ->
|
||||
|
||||
radioButtonProblemHtml = readFixtures('radiobutton_problem.html')
|
||||
checkboxProblemHtml = readFixtures('checkbox_problem.html')
|
||||
|
||||
beforeEach ->
|
||||
@problem = new Problem($('.xblock-student_view'))
|
||||
|
||||
@checkAssertionsAfterClickingAnotherOption = =>
|
||||
# verify that 'show answer button is no longer disabled'
|
||||
expect(@problem.el.find('.show').attr('disabled')).not.toEqual('disabled')
|
||||
|
||||
# verify that displayed answer disappears
|
||||
expect(@problem.el.find('div.choicegroup')).not.toHaveClass('choicegroup_correct')
|
||||
|
||||
# verify that radio/checkbox label has no span having class '.status.correct'
|
||||
expect(@problem.el.find('div.choicegroup')).not.toHaveAttr('span.status.correct')
|
||||
|
||||
it 'should become enabled after a radiobutton is selected', ->
|
||||
$('#input_example_1').replaceWith(radioButtonProblemHtml)
|
||||
# assume that 'ShowAnswer' button is clicked,
|
||||
# clicking make it disabled.
|
||||
@problem.el.find('.show').attr('disabled', 'disabled')
|
||||
# bind click event to input fields
|
||||
@problem.submitAnswersAndSubmitButton true
|
||||
# selects option 2
|
||||
$('#input_1_1_2').attr('checked', true).trigger('click')
|
||||
@checkAssertionsAfterClickingAnotherOption()
|
||||
|
||||
it 'should become enabled after a checkbox is selected', ->
|
||||
$('#input_example_1').replaceWith(checkboxProblemHtml)
|
||||
@problem.el.find('.show').attr('disabled', 'disabled')
|
||||
@problem.submitAnswersAndSubmitButton true
|
||||
$('#input_1_1_2').click()
|
||||
@checkAssertionsAfterClickingAnotherOption()
|
||||
1070
common/lib/xmodule/xmodule/js/spec/capa/display_spec.js
Normal file
1070
common/lib/xmodule/xmodule/js/spec/capa/display_spec.js
Normal file
@@ -0,0 +1,1070 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
describe('Problem', function() {
|
||||
const problem_content_default = readFixtures('problem_content.html');
|
||||
|
||||
beforeEach(function() {
|
||||
// Stub MathJax
|
||||
window.MathJax = {
|
||||
Hub: jasmine.createSpyObj('MathJax.Hub', ['getAllJax', 'Queue']),
|
||||
Callback: jasmine.createSpyObj('MathJax.Callback', ['After'])
|
||||
};
|
||||
this.stubbedJax = {root: jasmine.createSpyObj('jax.root', ['toMathML'])};
|
||||
MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]);
|
||||
window.update_schematics = function() {};
|
||||
spyOn(SR, 'readText');
|
||||
spyOn(SR, 'readTexts');
|
||||
|
||||
// Load this function from spec/helper.coffee
|
||||
// Note that if your test fails with a message like:
|
||||
// 'External request attempted for blah, which is not defined.'
|
||||
// this msg is coming from the stubRequests function else clause.
|
||||
jasmine.stubRequests();
|
||||
|
||||
loadFixtures('problem.html');
|
||||
|
||||
spyOn(Logger, 'log');
|
||||
spyOn($.fn, 'load').and.callFake(function(url, callback) {
|
||||
$(this).html(readFixtures('problem_content.html'));
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
|
||||
describe('constructor', function() {
|
||||
|
||||
it('set the element from html', function() {
|
||||
this.problem999 = new Problem((`\
|
||||
<section class='xblock xblock-student_view xmodule_display xmodule_CapaModule' data-type='Problem'> \
|
||||
<section id='problem_999' \
|
||||
class='problems-wrapper' \
|
||||
data-problem-id='i4x://edX/999/problem/Quiz' \
|
||||
data-url='/problem/quiz/'> \
|
||||
</section> \
|
||||
</section>\
|
||||
`)
|
||||
);
|
||||
expect(this.problem999.element_id).toBe('problem_999');
|
||||
});
|
||||
|
||||
it('set the element from loadFixtures', function() {
|
||||
this.problem1 = new Problem($('.xblock-student_view'));
|
||||
expect(this.problem1.element_id).toBe('problem_1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('bind', function() {
|
||||
beforeEach(function() {
|
||||
spyOn(window, 'update_schematics');
|
||||
MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]);
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
});
|
||||
|
||||
it('set mathjax typeset', () => expect(MathJax.Hub.Queue).toHaveBeenCalled());
|
||||
|
||||
it('update schematics', () => expect(window.update_schematics).toHaveBeenCalled());
|
||||
|
||||
it('bind answer refresh on button click', function() {
|
||||
expect($('div.action button')).toHandleWith('click', this.problem.refreshAnswers);
|
||||
});
|
||||
|
||||
it('bind the submit button', function() {
|
||||
expect($('.action .submit')).toHandleWith('click', this.problem.submit_fd);
|
||||
});
|
||||
|
||||
it('bind the reset button', function() {
|
||||
expect($('div.action button.reset')).toHandleWith('click', this.problem.reset);
|
||||
});
|
||||
|
||||
it('bind the show button', function() {
|
||||
expect($('.action .show')).toHandleWith('click', this.problem.show);
|
||||
});
|
||||
|
||||
it('bind the save button', function() {
|
||||
expect($('div.action button.save')).toHandleWith('click', this.problem.save);
|
||||
});
|
||||
|
||||
it('bind the math input', function() {
|
||||
expect($('input.math')).toHandleWith('keyup', this.problem.refreshMath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bind_with_custom_input_id', function() {
|
||||
beforeEach(function() {
|
||||
spyOn(window, 'update_schematics');
|
||||
MathJax.Hub.getAllJax.and.returnValue([this.stubbedJax]);
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
return $(this).html(readFixtures('problem_content_1240.html'));
|
||||
});
|
||||
|
||||
it('bind the submit button', function() {
|
||||
expect($('.action .submit')).toHandleWith('click', this.problem.submit_fd);
|
||||
});
|
||||
|
||||
it('bind the show button', function() {
|
||||
expect($('div.action button.show')).toHandleWith('click', this.problem.show);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('renderProgressState', function() {
|
||||
beforeEach(function() {
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
});
|
||||
|
||||
const testProgessData = function(problem, score, total_possible, attempts, graded, expected_progress_after_render) {
|
||||
problem.el.data('problem-score', score);
|
||||
problem.el.data('problem-total-possible', total_possible);
|
||||
problem.el.data('attempts-used', attempts);
|
||||
problem.el.data('graded', graded);
|
||||
expect(problem.$('.problem-progress').html()).toEqual("");
|
||||
problem.renderProgressState();
|
||||
expect(problem.$('.problem-progress').html()).toEqual(expected_progress_after_render);
|
||||
};
|
||||
|
||||
describe('with a status of "none"', function() {
|
||||
it('reports the number of points possible and graded', function() {
|
||||
testProgessData(this.problem, 0, 1, 0, "True", "1 point possible (graded)");
|
||||
});
|
||||
|
||||
it('displays the number of points possible when rendering happens with the content', function() {
|
||||
testProgessData(this.problem, 0, 2, 0, "True", "2 points possible (graded)");
|
||||
});
|
||||
|
||||
it('reports the number of points possible and ungraded', function() {
|
||||
testProgessData(this.problem, 0, 1, 0, "False", "1 point possible (ungraded)");
|
||||
});
|
||||
|
||||
it('displays ungraded if number of points possible is 0', function() {
|
||||
testProgessData(this.problem, 0, 0, 0, "False", "0 points possible (ungraded)");
|
||||
});
|
||||
|
||||
it('displays ungraded if number of points possible is 0, even if graded value is True', function() {
|
||||
testProgessData(this.problem, 0, 0, 0, "True", "0 points possible (ungraded)");
|
||||
});
|
||||
|
||||
it('reports the correct score with status none and >0 attempts', function() {
|
||||
testProgessData(this.problem, 0, 1, 1, "True", "0/1 point (graded)");
|
||||
});
|
||||
|
||||
it('reports the correct score with >1 weight, status none, and >0 attempts', function() {
|
||||
testProgessData(this.problem, 0, 2, 2, "True", "0/2 points (graded)");
|
||||
});
|
||||
});
|
||||
|
||||
describe('with any other valid status', function() {
|
||||
|
||||
it('reports the current score', function() {
|
||||
testProgessData(this.problem, 1, 1, 1, "True", "1/1 point (graded)");
|
||||
});
|
||||
|
||||
it('shows current score when rendering happens with the content', function() {
|
||||
testProgessData(this.problem, 2, 2, 1, "True", "2/2 points (graded)");
|
||||
});
|
||||
|
||||
it('reports the current score even if problem is ungraded', function() {
|
||||
testProgessData(this.problem, 1, 1, 1, "False", "1/1 point (ungraded)");
|
||||
});
|
||||
});
|
||||
|
||||
describe('with valid status and string containing an integer like "0" for detail', () =>
|
||||
// These tests are to address a failure specific to Chrome 51 and 52 +
|
||||
it('shows 0 points possible for the detail', function() {
|
||||
testProgessData(this.problem, 0, 0, 1, "False", "0 points possible (ungraded)");
|
||||
})
|
||||
);
|
||||
|
||||
describe('with a score of null (show_correctness == false)', function() {
|
||||
it('reports the number of points possible and graded, results hidden', function() {
|
||||
testProgessData(this.problem, null, 1, 0, "True", "1 point possible (graded, results hidden)");
|
||||
});
|
||||
|
||||
it('reports the number of points possible (plural) and graded, results hidden', function() {
|
||||
testProgessData(this.problem, null, 2, 0, "True", "2 points possible (graded, results hidden)");
|
||||
});
|
||||
|
||||
it('reports the number of points possible and ungraded, results hidden', function() {
|
||||
testProgessData(this.problem, null, 1, 0, "False", "1 point possible (ungraded, results hidden)");
|
||||
});
|
||||
|
||||
it('displays ungraded if number of points possible is 0, results hidden', function() {
|
||||
testProgessData(this.problem, null, 0, 0, "False", "0 points possible (ungraded, results hidden)");
|
||||
});
|
||||
|
||||
it('displays ungraded if number of points possible is 0, even if graded value is True, results hidden', function() {
|
||||
testProgessData(this.problem, null, 0, 0, "True", "0 points possible (ungraded, results hidden)");
|
||||
});
|
||||
|
||||
it('reports the correct score with status none and >0 attempts, results hidden', function() {
|
||||
testProgessData(this.problem, null, 1, 1, "True", "1 point possible (graded, results hidden)");
|
||||
});
|
||||
|
||||
it('reports the correct score with >1 weight, status none, and >0 attempts, results hidden', function() {
|
||||
testProgessData(this.problem, null, 2, 2, "True", "2 points possible (graded, results hidden)");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('render', function() {
|
||||
beforeEach(function() {
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
this.bind = this.problem.bind;
|
||||
spyOn(this.problem, 'bind');
|
||||
});
|
||||
|
||||
describe('with content given', function() {
|
||||
beforeEach(function() {
|
||||
this.problem.render('Hello World');
|
||||
});
|
||||
|
||||
it('render the content', function() {
|
||||
expect(this.problem.el.html()).toEqual('Hello World');
|
||||
});
|
||||
|
||||
it('re-bind the content', function() {
|
||||
expect(this.problem.bind).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no content given', function() {
|
||||
beforeEach(function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake((url, callback) => callback({html: "Hello World"}));
|
||||
this.problem.render();
|
||||
});
|
||||
|
||||
it('load the content via ajax', function() {
|
||||
expect(this.problem.el.html()).toEqual('Hello World');
|
||||
});
|
||||
|
||||
it('re-bind the content', function() {
|
||||
expect(this.problem.bind).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('submit_fd', function() {
|
||||
beforeEach(function() {
|
||||
// Insert an input of type file outside of the problem.
|
||||
$('.xblock-student_view').after('<input type="file" />');
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
spyOn(this.problem, 'submit');
|
||||
});
|
||||
|
||||
it('submit method is called if input of type file is not in problem', function() {
|
||||
this.problem.submit_fd();
|
||||
expect(this.problem.submit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('submit', function() {
|
||||
beforeEach(function() {
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
this.problem.answers = 'foo=1&bar=2';
|
||||
});
|
||||
|
||||
it('log the problem_check event', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
let promise;
|
||||
promise = {
|
||||
always(callable) { return callable(); },
|
||||
done(callable) { return callable(); }
|
||||
};
|
||||
return promise;
|
||||
});
|
||||
this.problem.submit();
|
||||
expect(Logger.log).toHaveBeenCalledWith('problem_check', 'foo=1&bar=2');
|
||||
});
|
||||
|
||||
it('log the problem_graded event, after the problem is done grading.', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
let promise;
|
||||
const response = {
|
||||
success: 'correct',
|
||||
contents: 'mock grader response'
|
||||
};
|
||||
callback(response);
|
||||
promise = {
|
||||
always(callable) { return callable(); },
|
||||
done(callable) { return callable(); }
|
||||
};
|
||||
return promise;
|
||||
});
|
||||
this.problem.submit();
|
||||
expect(Logger.log).toHaveBeenCalledWith('problem_graded', ['foo=1&bar=2', 'mock grader response'], this.problem.id);
|
||||
});
|
||||
|
||||
it('submit the answer for submit', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
let promise;
|
||||
promise = {
|
||||
always(callable) { return callable(); },
|
||||
done(callable) { return callable(); }
|
||||
};
|
||||
return promise;
|
||||
});
|
||||
this.problem.submit();
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith('/problem/Problem1/problem_check',
|
||||
'foo=1&bar=2', jasmine.any(Function));
|
||||
});
|
||||
|
||||
describe('when the response is correct', () =>
|
||||
it('call render with returned content', function() {
|
||||
const contents = '<div class="wrapper-problem-response" aria-label="Question 1"><p>Correct<span class="status">excellent</span></p></div>' +
|
||||
'<div class="wrapper-problem-response" aria-label="Question 2"><p>Yep<span class="status">correct</span></p></div>';
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
let promise;
|
||||
callback({success: 'correct', contents});
|
||||
promise = {
|
||||
always(callable) { return callable(); },
|
||||
done(callable) { return callable(); }
|
||||
};
|
||||
return promise;
|
||||
});
|
||||
this.problem.submit();
|
||||
expect(this.problem.el).toHaveHtml(contents);
|
||||
expect(window.SR.readTexts).toHaveBeenCalledWith(['Question 1: excellent', 'Question 2: correct']);
|
||||
})
|
||||
);
|
||||
|
||||
describe('when the response is incorrect', () =>
|
||||
it('call render with returned content', function() {
|
||||
const contents = '<p>Incorrect<span class="status">no, try again</span></p>';
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
let promise;
|
||||
callback({success: 'incorrect', contents});
|
||||
promise = {
|
||||
always(callable) { return callable(); },
|
||||
done(callable) { return callable(); }
|
||||
};
|
||||
return promise;
|
||||
});
|
||||
this.problem.submit();
|
||||
expect(this.problem.el).toHaveHtml(contents);
|
||||
expect(window.SR.readTexts).toHaveBeenCalledWith(['no, try again']);
|
||||
})
|
||||
);
|
||||
|
||||
it('tests if the submit button is disabled while submitting and the text changes on the button', function() {
|
||||
const self = this;
|
||||
const curr_html = this.problem.el.html();
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
// At this point enableButtons should have been called, making the submit button disabled with text 'submitting'
|
||||
let promise;
|
||||
expect(self.problem.submitButton).toHaveAttr('disabled');
|
||||
expect(self.problem.submitButtonLabel.text()).toBe('Submitting');
|
||||
callback({
|
||||
success: 'incorrect', // does not matter if correct or incorrect here
|
||||
contents: curr_html
|
||||
});
|
||||
promise = {
|
||||
always(callable) { return callable(); },
|
||||
done(callable) { return callable(); }
|
||||
};
|
||||
return promise;
|
||||
});
|
||||
// Make sure the submit button is enabled before submitting
|
||||
$('#input_example_1').val('test').trigger('input');
|
||||
expect(this.problem.submitButton).not.toHaveAttr('disabled');
|
||||
this.problem.submit();
|
||||
// After submit, the button should not be disabled and should have text as 'Submit'
|
||||
expect(this.problem.submitButtonLabel.text()).toBe('Submit');
|
||||
expect(this.problem.submitButton).not.toHaveAttr('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('submit button on problems', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
this.submitDisabled = disabled => {
|
||||
if (disabled) {
|
||||
expect(this.problem.submitButton).toHaveAttr('disabled');
|
||||
} else {
|
||||
expect(this.problem.submitButton).not.toHaveAttr('disabled');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
describe('some basic tests for submit button', () =>
|
||||
it('should become enabled after a value is entered into the text box', function() {
|
||||
$('#input_example_1').val('test').trigger('input');
|
||||
this.submitDisabled(false);
|
||||
$('#input_example_1').val('').trigger('input');
|
||||
this.submitDisabled(true);
|
||||
})
|
||||
);
|
||||
|
||||
describe('some advanced tests for submit button', function() {
|
||||
const radioButtonProblemHtml = readFixtures('radiobutton_problem.html');
|
||||
const checkboxProblemHtml = readFixtures('checkbox_problem.html');
|
||||
|
||||
it('should become enabled after a checkbox is checked', function() {
|
||||
$('#input_example_1').replaceWith(checkboxProblemHtml);
|
||||
this.problem.submitAnswersAndSubmitButton(true);
|
||||
this.submitDisabled(true);
|
||||
$('#input_1_1_1').click();
|
||||
this.submitDisabled(false);
|
||||
$('#input_1_1_1').click();
|
||||
this.submitDisabled(true);
|
||||
});
|
||||
|
||||
it('should become enabled after a radiobutton is checked', function() {
|
||||
$('#input_example_1').replaceWith(radioButtonProblemHtml);
|
||||
this.problem.submitAnswersAndSubmitButton(true);
|
||||
this.submitDisabled(true);
|
||||
$('#input_1_1_1').attr('checked', true).trigger('click');
|
||||
this.submitDisabled(false);
|
||||
$('#input_1_1_1').attr('checked', false).trigger('click');
|
||||
this.submitDisabled(true);
|
||||
});
|
||||
|
||||
it('should become enabled after a value is selected in a selector', function() {
|
||||
const html = `\
|
||||
<div id="problem_sel">
|
||||
<select>
|
||||
<option value="val0">Select an option</option>
|
||||
<option value="val1">1</option>
|
||||
<option value="val2">2</option>
|
||||
</select>
|
||||
</div>\
|
||||
`;
|
||||
$('#input_example_1').replaceWith(html);
|
||||
this.problem.submitAnswersAndSubmitButton(true);
|
||||
this.submitDisabled(true);
|
||||
$("#problem_sel select").val("val2").trigger('change');
|
||||
this.submitDisabled(false);
|
||||
$("#problem_sel select").val("val0").trigger('change');
|
||||
this.submitDisabled(true);
|
||||
});
|
||||
|
||||
it('should become enabled after a radiobutton is checked and a value is entered into the text box', function() {
|
||||
$(radioButtonProblemHtml).insertAfter('#input_example_1');
|
||||
this.problem.submitAnswersAndSubmitButton(true);
|
||||
this.submitDisabled(true);
|
||||
$('#input_1_1_1').attr('checked', true).trigger('click');
|
||||
this.submitDisabled(true);
|
||||
$('#input_example_1').val('111').trigger('input');
|
||||
this.submitDisabled(false);
|
||||
$('#input_1_1_1').attr('checked', false).trigger('click');
|
||||
this.submitDisabled(true);
|
||||
});
|
||||
|
||||
it('should become enabled if there are only hidden input fields', function() {
|
||||
const html = `\
|
||||
<input type="text" name="test" id="test" aria-describedby="answer_test" value="" style="display:none;">\
|
||||
`;
|
||||
$('#input_example_1').replaceWith(html);
|
||||
this.problem.submitAnswersAndSubmitButton(true);
|
||||
this.submitDisabled(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', function() {
|
||||
beforeEach(function() {
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
});
|
||||
|
||||
it('log the problem_reset event', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
let promise;
|
||||
promise =
|
||||
{always(callable) { return callable(); }};
|
||||
return promise;
|
||||
});
|
||||
this.problem.answers = 'foo=1&bar=2';
|
||||
this.problem.reset();
|
||||
expect(Logger.log).toHaveBeenCalledWith('problem_reset', 'foo=1&bar=2');
|
||||
});
|
||||
|
||||
it('POST to the problem reset page', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
let promise;
|
||||
promise =
|
||||
{always(callable) { return callable(); }};
|
||||
return promise;
|
||||
});
|
||||
this.problem.reset();
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith('/problem/Problem1/problem_reset',
|
||||
{ id: 'i4x://edX/101/problem/Problem1' }, jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('render the returned content', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
let promise;
|
||||
callback({html: "Reset", success: true});
|
||||
promise =
|
||||
{always(callable) { return callable(); }};
|
||||
return promise;
|
||||
});
|
||||
this.problem.reset();
|
||||
expect(this.problem.el.html()).toEqual('Reset');
|
||||
});
|
||||
|
||||
it('sends a message to the window SR element', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
let promise;
|
||||
callback({html: "Reset", success: true});
|
||||
promise =
|
||||
{always(callable) { return callable(); }};
|
||||
return promise;
|
||||
});
|
||||
this.problem.reset();
|
||||
expect(window.SR.readText).toHaveBeenCalledWith('This problem has been reset.');
|
||||
});
|
||||
|
||||
it('shows a notification on error', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
let promise;
|
||||
callback({msg: "Error on reset.", success: false});
|
||||
promise =
|
||||
{always(callable) { return callable(); }};
|
||||
return promise;
|
||||
});
|
||||
this.problem.reset();
|
||||
expect($('.notification-gentle-alert .notification-message').text()).toEqual("Error on reset.");
|
||||
});
|
||||
|
||||
it('tests that reset does not enable submit or modify the text while resetting', function() {
|
||||
const self = this;
|
||||
const curr_html = this.problem.el.html();
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
// enableButtons should have been called at this point to set them to all disabled
|
||||
let promise;
|
||||
expect(self.problem.submitButton).toHaveAttr('disabled');
|
||||
expect(self.problem.submitButtonLabel.text()).toBe('Submit');
|
||||
callback({success: 'correct', html: curr_html});
|
||||
promise =
|
||||
{always(callable) { return callable(); }};
|
||||
return promise;
|
||||
});
|
||||
// Submit should be disabled
|
||||
expect(this.problem.submitButton).toHaveAttr('disabled');
|
||||
this.problem.reset();
|
||||
// Submit should remain disabled
|
||||
expect(self.problem.submitButton).toHaveAttr('disabled');
|
||||
expect(self.problem.submitButtonLabel.text()).toBe('Submit');
|
||||
});
|
||||
});
|
||||
|
||||
describe('show', function() {
|
||||
beforeEach(function() {
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
this.problem.el.prepend('<div id="answer_1_1" /><div id="answer_1_2" />');
|
||||
});
|
||||
|
||||
describe('when the answer has not yet shown', function() {
|
||||
beforeEach(function() {
|
||||
expect(this.problem.el.find('.show').attr('disabled')).not.toEqual('disabled');
|
||||
});
|
||||
|
||||
it('log the problem_show event', function() {
|
||||
this.problem.show();
|
||||
expect(Logger.log).toHaveBeenCalledWith('problem_show',
|
||||
{problem: 'i4x://edX/101/problem/Problem1'});
|
||||
});
|
||||
|
||||
it('fetch the answers', function() {
|
||||
spyOn($, 'postWithPrefix');
|
||||
this.problem.show();
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith('/problem/Problem1/problem_show',
|
||||
jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('show the answers', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake((url, callback) => callback({answers: {'1_1': 'One', '1_2': 'Two'}}));
|
||||
this.problem.show();
|
||||
expect($('#answer_1_1')).toHaveHtml('One');
|
||||
expect($('#answer_1_2')).toHaveHtml('Two');
|
||||
});
|
||||
|
||||
it('disables the show answer button', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake((url, callback) => callback({answers: {}}));
|
||||
this.problem.show();
|
||||
expect(this.problem.el.find('.show').attr('disabled')).toEqual('disabled');
|
||||
});
|
||||
|
||||
describe('radio text question', function() {
|
||||
const radio_text_xml=`\
|
||||
<section class="problem">
|
||||
<div><p></p><span><section id="choicetextinput_1_2_1" class="choicetextinput">
|
||||
|
||||
<form class="choicetextgroup capa_inputtype" id="inputtype_1_2_1">
|
||||
<div class="indicator-container">
|
||||
<span class="unanswered" style="display:inline-block;" id="status_1_2_1"></span>
|
||||
</div>
|
||||
<fieldset>
|
||||
<section id="forinput1_2_1_choiceinput_0bc">
|
||||
<input class="ctinput" type="radio" name="choiceinput_1_2_1" id="1_2_1_choiceinput_0bc" value="choiceinput_0"">
|
||||
<input class="ctinput" type="text" name="choiceinput_0_textinput_0" id="1_2_1_choiceinput_0_textinput_0" value=" ">
|
||||
<p id="answer_1_2_1_choiceinput_0bc" class="answer"></p>
|
||||
</>
|
||||
<section id="forinput1_2_1_choiceinput_1bc">
|
||||
<input class="ctinput" type="radio" name="choiceinput_1_2_1" id="1_2_1_choiceinput_1bc" value="choiceinput_1" >
|
||||
<input class="ctinput" type="text" name="choiceinput_1_textinput_0" id="1_2_1_choiceinput_1_textinput_0" value=" " >
|
||||
<p id="answer_1_2_1_choiceinput_1bc" class="answer"></p>
|
||||
</section>
|
||||
<section id="forinput1_2_1_choiceinput_2bc">
|
||||
<input class="ctinput" type="radio" name="choiceinput_1_2_1" id="1_2_1_choiceinput_2bc" value="choiceinput_2" >
|
||||
<input class="ctinput" type="text" name="choiceinput_2_textinput_0" id="1_2_1_choiceinput_2_textinput_0" value=" " >
|
||||
<p id="answer_1_2_1_choiceinput_2bc" class="answer"></p>
|
||||
</section></fieldset><input class="choicetextvalue" type="hidden" name="input_1_2_1" id="input_1_2_1"></form>
|
||||
</section></span></div>
|
||||
</section>\
|
||||
`;
|
||||
beforeEach(function() {
|
||||
// Append a radiotextresponse problem to the problem, so we can check it's javascript functionality
|
||||
this.problem.el.prepend(radio_text_xml);
|
||||
});
|
||||
|
||||
it('sets the correct class on the section for the correct choice', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake((url, callback) => callback({answers: {"1_2_1": ["1_2_1_choiceinput_0bc"], "1_2_1_choiceinput_0bc": "3"}}));
|
||||
this.problem.show();
|
||||
|
||||
expect($('#forinput1_2_1_choiceinput_0bc').attr('class')).toEqual(
|
||||
'choicetextgroup_show_correct');
|
||||
expect($('#answer_1_2_1_choiceinput_0bc').text()).toEqual('3');
|
||||
expect($('#answer_1_2_1_choiceinput_1bc').text()).toEqual('');
|
||||
expect($('#answer_1_2_1_choiceinput_2bc').text()).toEqual('');
|
||||
});
|
||||
|
||||
it('Should not disable input fields', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake((url, callback) => callback({answers: {"1_2_1": ["1_2_1_choiceinput_0bc"], "1_2_1_choiceinput_0bc": "3"}}));
|
||||
this.problem.show();
|
||||
expect($('input#1_2_1_choiceinput_0bc').attr('disabled')).not.toEqual('disabled');
|
||||
expect($('input#1_2_1_choiceinput_1bc').attr('disabled')).not.toEqual('disabled');
|
||||
expect($('input#1_2_1_choiceinput_2bc').attr('disabled')).not.toEqual('disabled');
|
||||
expect($('input#1_2_1').attr('disabled')).not.toEqual('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('imageinput', function() {
|
||||
let el, height, width;
|
||||
const imageinput_html = readFixtures('imageinput.underscore');
|
||||
|
||||
const DEFAULTS = {
|
||||
id: '12345',
|
||||
width: '300',
|
||||
height: '400'
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
this.problem.el.prepend(_.template(imageinput_html)(DEFAULTS));
|
||||
});
|
||||
|
||||
const assertAnswer = (problem, data) => {
|
||||
stubRequest(data);
|
||||
problem.show();
|
||||
|
||||
$.each(data['answers'], (id, answer) => {
|
||||
const img = getImage(answer);
|
||||
el = $(`#inputtype_${id}`);
|
||||
expect(img).toImageDiffEqual(el.find('canvas')[0]);
|
||||
});
|
||||
};
|
||||
|
||||
var stubRequest = data => {
|
||||
spyOn($, 'postWithPrefix').and.callFake((url, callback) => callback(data));
|
||||
};
|
||||
|
||||
var getImage = (coords, c_width, c_height) => {
|
||||
let ctx, reg;
|
||||
const types = {
|
||||
rectangle: coords => {
|
||||
reg = /^\(([0-9]+),([0-9]+)\)-\(([0-9]+),([0-9]+)\)$/;
|
||||
const rects = coords.replace(/\s*/g, '').split(/;/);
|
||||
|
||||
$.each(rects, (index, rect) => {
|
||||
const { abs } = Math;
|
||||
const points = reg.exec(rect);
|
||||
if (points) {
|
||||
width = abs(points[3] - points[1]);
|
||||
height = abs(points[4] - points[2]);
|
||||
|
||||
return ctx.rect(points[1], points[2], width, height);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.stroke();
|
||||
ctx.fill();
|
||||
},
|
||||
|
||||
regions: coords => {
|
||||
const parseCoords = coords => {
|
||||
reg = JSON.parse(coords);
|
||||
|
||||
if (typeof reg[0][0][0] === "undefined") {
|
||||
reg = [reg];
|
||||
}
|
||||
|
||||
return reg;
|
||||
};
|
||||
|
||||
return $.each(parseCoords(coords), (index, region) => {
|
||||
ctx.beginPath();
|
||||
$.each(region, (index, point) => {
|
||||
if (index === 0) {
|
||||
return ctx.moveTo(point[0], point[1]);
|
||||
} else {
|
||||
return ctx.lineTo(point[0], point[1]);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = c_width || 100;
|
||||
canvas.height = c_height || 100;
|
||||
|
||||
if (canvas.getContext) {
|
||||
ctx = canvas.getContext('2d');
|
||||
} else {
|
||||
console.log('Canvas is not supported.');
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,.3)';
|
||||
ctx.strokeStyle = "#FF0000";
|
||||
ctx.lineWidth = "2";
|
||||
|
||||
$.each(coords, (key, value) => {
|
||||
if ((types[key] != null) && value) { return types[key](value); }
|
||||
});
|
||||
|
||||
return canvas;
|
||||
};
|
||||
|
||||
it('rectangle is drawn correctly', function() {
|
||||
assertAnswer(this.problem, {
|
||||
'answers': {
|
||||
'12345': {
|
||||
'rectangle': '(10,10)-(30,30)',
|
||||
'regions': null
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('region is drawn correctly', function() {
|
||||
assertAnswer(this.problem, {
|
||||
'answers': {
|
||||
'12345': {
|
||||
'rectangle': null,
|
||||
'regions': '[[10,10],[30,30],[70,30],[20,30]]'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('mixed shapes are drawn correctly', function() {
|
||||
assertAnswer(this.problem, {
|
||||
'answers': {'12345': {
|
||||
'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]]
|
||||
]`
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('multiple image inputs draw answers on separate canvases', function() {
|
||||
const data = {
|
||||
id: '67890',
|
||||
width: '400',
|
||||
height: '300'
|
||||
};
|
||||
|
||||
this.problem.el.prepend(_.template(imageinput_html)(data));
|
||||
assertAnswer(this.problem, {
|
||||
'answers': {
|
||||
'12345': {
|
||||
'rectangle': null,
|
||||
'regions': '[[10,10],[30,30],[70,30],[20,30]]'
|
||||
},
|
||||
'67890': {
|
||||
'rectangle': '(10,10)-(30,30)',
|
||||
'regions': null
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('dictionary with answers doesn\'t contain answer for current id', function() {
|
||||
spyOn(console, 'log');
|
||||
stubRequest({'answers':{}});
|
||||
this.problem.show();
|
||||
el = $('#inputtype_12345');
|
||||
expect(el.find('canvas')).not.toExist();
|
||||
expect(console.log).toHaveBeenCalledWith('Answer is absent for image input with id=12345');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('save', function() {
|
||||
beforeEach(function() {
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
this.problem.answers = 'foo=1&bar=2';
|
||||
});
|
||||
|
||||
it('log the problem_save event', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
let promise;
|
||||
promise =
|
||||
{always(callable) { return callable(); }};
|
||||
return promise;
|
||||
});
|
||||
this.problem.save();
|
||||
expect(Logger.log).toHaveBeenCalledWith('problem_save', 'foo=1&bar=2');
|
||||
});
|
||||
|
||||
it('POST to save problem', function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
let promise;
|
||||
promise =
|
||||
{always(callable) { return callable(); }};
|
||||
return promise;
|
||||
});
|
||||
this.problem.save();
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith('/problem/Problem1/problem_save',
|
||||
'foo=1&bar=2', jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('tests that save does not enable the submit button or change the text when submit is originally disabled', function() {
|
||||
const self = this;
|
||||
const curr_html = this.problem.el.html();
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
// enableButtons should have been called at this point and the submit button should be unaffected
|
||||
let promise;
|
||||
expect(self.problem.submitButton).toHaveAttr('disabled');
|
||||
expect(self.problem.submitButtonLabel.text()).toBe('Submit');
|
||||
callback({success: 'correct', html: curr_html});
|
||||
promise =
|
||||
{always(callable) { return callable(); }};
|
||||
return promise;
|
||||
});
|
||||
// Expect submit to be disabled and labeled properly at the start
|
||||
expect(this.problem.submitButton).toHaveAttr('disabled');
|
||||
expect(this.problem.submitButtonLabel.text()).toBe('Submit');
|
||||
this.problem.save();
|
||||
// Submit button should have the same state after save has completed
|
||||
expect(this.problem.submitButton).toHaveAttr('disabled');
|
||||
expect(this.problem.submitButtonLabel.text()).toBe('Submit');
|
||||
});
|
||||
|
||||
it('tests that save does not disable the submit button or change the text when submit is originally enabled', function() {
|
||||
const self = this;
|
||||
const curr_html = this.problem.el.html();
|
||||
spyOn($, 'postWithPrefix').and.callFake(function(url, answers, callback) {
|
||||
// enableButtons should have been called at this point, and the submit button should be disabled while submitting
|
||||
let promise;
|
||||
expect(self.problem.submitButton).toHaveAttr('disabled');
|
||||
expect(self.problem.submitButtonLabel.text()).toBe('Submit');
|
||||
callback({success: 'correct', html: curr_html});
|
||||
promise =
|
||||
{always(callable) { return callable(); }};
|
||||
return promise;
|
||||
});
|
||||
// Expect submit to be enabled and labeled properly at the start after adding an input
|
||||
$('#input_example_1').val('test').trigger('input');
|
||||
expect(this.problem.submitButton).not.toHaveAttr('disabled');
|
||||
expect(this.problem.submitButtonLabel.text()).toBe('Submit');
|
||||
this.problem.save();
|
||||
// Submit button should have the same state after save has completed
|
||||
expect(this.problem.submitButton).not.toHaveAttr('disabled');
|
||||
expect(this.problem.submitButtonLabel.text()).toBe('Submit');
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshMath', function() {
|
||||
beforeEach(function() {
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
$('#input_example_1').val('E=mc^2');
|
||||
this.problem.refreshMath({target: $('#input_example_1').get(0)});
|
||||
});
|
||||
|
||||
it('should queue the conversion and MathML element update', function() {
|
||||
expect(MathJax.Hub.Queue).toHaveBeenCalledWith(['Text', this.stubbedJax, 'E=mc^2'],
|
||||
[this.problem.updateMathML, this.stubbedJax, $('#input_example_1').get(0)]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMathML', function() {
|
||||
beforeEach(function() {
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
this.stubbedJax.root.toMathML.and.returnValue('<MathML>');
|
||||
});
|
||||
|
||||
describe('when there is no exception', function() {
|
||||
beforeEach(function() {
|
||||
this.problem.updateMathML(this.stubbedJax, $('#input_example_1').get(0));
|
||||
});
|
||||
|
||||
it('convert jax to MathML', () => expect($('#input_example_1_dynamath')).toHaveValue('<MathML>'));
|
||||
});
|
||||
|
||||
describe('when there is an exception', function() {
|
||||
beforeEach(function() {
|
||||
const error = new Error();
|
||||
error.restart = true;
|
||||
this.stubbedJax.root.toMathML.and.throwError(error);
|
||||
this.problem.updateMathML(this.stubbedJax, $('#input_example_1').get(0));
|
||||
});
|
||||
|
||||
it('should queue up the exception', function() {
|
||||
expect(MathJax.Callback.After).toHaveBeenCalledWith([this.problem.refreshMath, this.stubbedJax], true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshAnswers', function() {
|
||||
beforeEach(function() {
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
this.problem.el.html(`\
|
||||
<textarea class="CodeMirror" />
|
||||
<input id="input_1_1" name="input_1_1" class="schematic" value="one" />
|
||||
<input id="input_1_2" name="input_1_2" value="two" />
|
||||
<input id="input_bogus_3" name="input_bogus_3" value="three" />\
|
||||
`
|
||||
);
|
||||
this.stubSchematic = { update_value: jasmine.createSpy('schematic') };
|
||||
this.stubCodeMirror = { save: jasmine.createSpy('CodeMirror') };
|
||||
$('input.schematic').get(0).schematic = this.stubSchematic;
|
||||
$('textarea.CodeMirror').get(0).CodeMirror = this.stubCodeMirror;
|
||||
});
|
||||
|
||||
it('update each schematic', function() {
|
||||
this.problem.refreshAnswers();
|
||||
expect(this.stubSchematic.update_value).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('update each code block', function() {
|
||||
this.problem.refreshAnswers();
|
||||
expect(this.stubCodeMirror.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple JsInput in single problem', function() {
|
||||
const jsinput_html = readFixtures('jsinput_problem.html');
|
||||
|
||||
beforeEach(function() {
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
this.problem.render(jsinput_html);
|
||||
});
|
||||
|
||||
it('submit_save_waitfor should return false', function() {
|
||||
$(this.problem.inputs[0]).data('waitfor', function() {});
|
||||
expect(this.problem.submit_save_waitfor()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Submitting an xqueue-graded problem', function() {
|
||||
const matlabinput_html = readFixtures('matlabinput_problem.html');
|
||||
|
||||
beforeEach(function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake((url, callback) => callback({html: matlabinput_html}));
|
||||
jasmine.clock().install();
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
spyOn(this.problem, 'poll').and.callThrough();
|
||||
this.problem.render(matlabinput_html);
|
||||
});
|
||||
|
||||
afterEach(() => jasmine.clock().uninstall());
|
||||
|
||||
it('check that we stop polling after a fixed amount of time', function() {
|
||||
expect(this.problem.poll).not.toHaveBeenCalled();
|
||||
jasmine.clock().tick(1);
|
||||
const time_steps = [1000, 2000, 4000, 8000, 16000, 32000];
|
||||
let num_calls = 1;
|
||||
for (let time_step of Array.from(time_steps)) {
|
||||
(time_step => {
|
||||
jasmine.clock().tick(time_step);
|
||||
expect(this.problem.poll.calls.count()).toEqual(num_calls);
|
||||
num_calls += 1;
|
||||
})(time_step);
|
||||
}
|
||||
|
||||
// jump the next step and verify that we are not still continuing to poll
|
||||
jasmine.clock().tick(64000);
|
||||
expect(this.problem.poll.calls.count()).toEqual(6);
|
||||
|
||||
expect($('.notification-gentle-alert .notification-message').text()).toEqual("The grading process is still running. Refresh the page to see updates.");
|
||||
});
|
||||
});
|
||||
|
||||
describe('codeinput problem', function() {
|
||||
const codeinputProblemHtml = readFixtures('codeinput_problem.html');
|
||||
|
||||
beforeEach(function() {
|
||||
spyOn($, 'postWithPrefix').and.callFake((url, callback) => callback({html: codeinputProblemHtml}));
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
this.problem.render(codeinputProblemHtml);
|
||||
});
|
||||
|
||||
it('has rendered with correct a11y info', function() {
|
||||
const CodeMirrorTextArea = $('textarea')[1];
|
||||
const CodeMirrorTextAreaId = 'cm-textarea-101';
|
||||
|
||||
// verify that question label has correct `for` attribute value
|
||||
expect($('.problem-group-label').attr('for')).toEqual(CodeMirrorTextAreaId);
|
||||
|
||||
// verify that codemirror textarea has correct `id` attribute value
|
||||
expect($(CodeMirrorTextArea).attr('id')).toEqual(CodeMirrorTextAreaId);
|
||||
|
||||
// verify that codemirror textarea has correct `aria-describedby` attribute value
|
||||
expect($(CodeMirrorTextArea).attr('aria-describedby')).toEqual('cm-editor-exit-message-101 status_101');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('show answer button', function() {
|
||||
|
||||
const radioButtonProblemHtml = readFixtures('radiobutton_problem.html');
|
||||
const checkboxProblemHtml = readFixtures('checkbox_problem.html');
|
||||
|
||||
beforeEach(function() {
|
||||
this.problem = new Problem($('.xblock-student_view'));
|
||||
|
||||
this.checkAssertionsAfterClickingAnotherOption = () => {
|
||||
// verify that 'show answer button is no longer disabled'
|
||||
expect(this.problem.el.find('.show').attr('disabled')).not.toEqual('disabled');
|
||||
|
||||
// verify that displayed answer disappears
|
||||
expect(this.problem.el.find('div.choicegroup')).not.toHaveClass('choicegroup_correct');
|
||||
|
||||
// verify that radio/checkbox label has no span having class '.status.correct'
|
||||
expect(this.problem.el.find('div.choicegroup')).not.toHaveAttr('span.status.correct');
|
||||
};
|
||||
});
|
||||
|
||||
it('should become enabled after a radiobutton is selected', function() {
|
||||
$('#input_example_1').replaceWith(radioButtonProblemHtml);
|
||||
// assume that 'ShowAnswer' button is clicked,
|
||||
// clicking make it disabled.
|
||||
this.problem.el.find('.show').attr('disabled', 'disabled');
|
||||
// bind click event to input fields
|
||||
this.problem.submitAnswersAndSubmitButton(true);
|
||||
// selects option 2
|
||||
$('#input_1_1_2').attr('checked', true).trigger('click');
|
||||
this.checkAssertionsAfterClickingAnotherOption();
|
||||
});
|
||||
|
||||
it('should become enabled after a checkbox is selected', function() {
|
||||
$('#input_example_1').replaceWith(checkboxProblemHtml);
|
||||
this.problem.el.find('.show').attr('disabled', 'disabled');
|
||||
this.problem.submitAnswersAndSubmitButton(true);
|
||||
$('#input_1_1_2').click();
|
||||
this.checkAssertionsAfterClickingAnotherOption();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
describe 'HTMLEditingDescriptor', ->
|
||||
beforeEach ->
|
||||
window.baseUrl = "/static/deadbeef"
|
||||
afterEach ->
|
||||
delete window.baseUrl
|
||||
describe 'Visual HTML Editor', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'html-edit-visual.html'
|
||||
@descriptor = new HTMLEditingDescriptor($('.test-component'))
|
||||
it 'Returns data from Visual Editor if text has changed', ->
|
||||
visualEditorStub =
|
||||
getContent: () -> 'from visual editor'
|
||||
spyOn(@descriptor, 'getVisualEditor').and.callFake () ->
|
||||
visualEditorStub
|
||||
data = @descriptor.save().data
|
||||
expect(data).toEqual('from visual editor')
|
||||
it 'Returns data from Raw Editor if text has not changed', ->
|
||||
visualEditorStub =
|
||||
getContent: () -> '<p>original visual text</p>'
|
||||
spyOn(@descriptor, 'getVisualEditor').and.callFake () ->
|
||||
visualEditorStub
|
||||
data = @descriptor.save().data
|
||||
expect(data).toEqual('raw text')
|
||||
it 'Performs link rewriting for static assets when saving', ->
|
||||
visualEditorStub =
|
||||
getContent: () -> 'from visual editor with /c4x/foo/bar/asset/image.jpg'
|
||||
spyOn(@descriptor, 'getVisualEditor').and.callFake () ->
|
||||
visualEditorStub
|
||||
data = @descriptor.save().data
|
||||
expect(data).toEqual('from visual editor with /static/image.jpg')
|
||||
it 'When showing visual editor links are rewritten to c4x format', ->
|
||||
visualEditorStub =
|
||||
content: 'text /static/image.jpg'
|
||||
startContent: 'text /static/image.jpg'
|
||||
focus: ->
|
||||
setContent: (x) -> @content = x
|
||||
getContent: -> @content
|
||||
|
||||
@descriptor.initInstanceCallback(visualEditorStub)
|
||||
expect(visualEditorStub.getContent()).toEqual('text /c4x/foo/bar/asset/image.jpg')
|
||||
it 'Enables spellcheck', ->
|
||||
expect($('.html-editor iframe')[0].contentDocument.body.spellcheck).toBe(true)
|
||||
describe 'Raw HTML Editor', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'html-editor-raw.html'
|
||||
@descriptor = new HTMLEditingDescriptor($('.test-component'))
|
||||
it 'Returns data from raw editor', ->
|
||||
data = @descriptor.save().data
|
||||
expect(data).toEqual('raw text')
|
||||
54
common/lib/xmodule/xmodule/js/spec/html/edit_spec.js
Normal file
54
common/lib/xmodule/xmodule/js/spec/html/edit_spec.js
Normal file
@@ -0,0 +1,54 @@
|
||||
describe('HTMLEditingDescriptor', function() {
|
||||
beforeEach(() => window.baseUrl = "/static/deadbeef");
|
||||
afterEach(() => delete window.baseUrl);
|
||||
describe('Visual HTML Editor', function() {
|
||||
beforeEach(function() {
|
||||
loadFixtures('html-edit-visual.html');
|
||||
this.descriptor = new HTMLEditingDescriptor($('.test-component'));
|
||||
});
|
||||
it('Returns data from Visual Editor if text has changed', function() {
|
||||
const visualEditorStub =
|
||||
{getContent() { return 'from visual editor'; }};
|
||||
spyOn(this.descriptor, 'getVisualEditor').and.callFake(() => visualEditorStub);
|
||||
const { data } = this.descriptor.save();
|
||||
expect(data).toEqual('from visual editor');
|
||||
});
|
||||
it('Returns data from Raw Editor if text has not changed', function() {
|
||||
const visualEditorStub =
|
||||
{getContent() { return '<p>original visual text</p>'; }};
|
||||
spyOn(this.descriptor, 'getVisualEditor').and.callFake(() => visualEditorStub);
|
||||
const { data } = this.descriptor.save();
|
||||
expect(data).toEqual('raw text');
|
||||
});
|
||||
it('Performs link rewriting for static assets when saving', function() {
|
||||
const visualEditorStub =
|
||||
{getContent() { return 'from visual editor with /c4x/foo/bar/asset/image.jpg'; }};
|
||||
spyOn(this.descriptor, 'getVisualEditor').and.callFake(() => visualEditorStub);
|
||||
const { data } = this.descriptor.save();
|
||||
expect(data).toEqual('from visual editor with /static/image.jpg');
|
||||
});
|
||||
it('When showing visual editor links are rewritten to c4x format', function() {
|
||||
const visualEditorStub = {
|
||||
content: 'text /static/image.jpg',
|
||||
startContent: 'text /static/image.jpg',
|
||||
focus() {},
|
||||
setContent(x) { this.content = x; },
|
||||
getContent() { return this.content; }
|
||||
};
|
||||
|
||||
this.descriptor.initInstanceCallback(visualEditorStub);
|
||||
expect(visualEditorStub.getContent()).toEqual('text /c4x/foo/bar/asset/image.jpg');
|
||||
});
|
||||
it('Enables spellcheck', () => expect($('.html-editor iframe')[0].contentDocument.body.spellcheck).toBe(true));
|
||||
});
|
||||
describe('Raw HTML Editor', function() {
|
||||
beforeEach(function() {
|
||||
loadFixtures('html-editor-raw.html');
|
||||
this.descriptor = new HTMLEditingDescriptor($('.test-component'));
|
||||
});
|
||||
it('Returns data from raw editor', function() {
|
||||
const { data } = this.descriptor.save();
|
||||
expect(data).toEqual('raw text');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,985 +0,0 @@
|
||||
describe 'MarkdownEditingDescriptor', ->
|
||||
describe 'save stores the correct data', ->
|
||||
it 'saves markdown from markdown editor', ->
|
||||
loadFixtures 'problem-with-markdown.html'
|
||||
@descriptor = new MarkdownEditingDescriptor($('.problem-editor'))
|
||||
saveResult = @descriptor.save()
|
||||
expect(saveResult.metadata.markdown).toEqual('markdown')
|
||||
expect(saveResult.data).toXMLEqual('<problem>\n <p>markdown</p>\n</problem>')
|
||||
it 'clears markdown when xml editor is selected', ->
|
||||
loadFixtures 'problem-with-markdown.html'
|
||||
@descriptor = new MarkdownEditingDescriptor($('.problem-editor'))
|
||||
@descriptor.createXMLEditor('replace with markdown')
|
||||
saveResult = @descriptor.save()
|
||||
expect(saveResult.nullout).toEqual(['markdown'])
|
||||
expect(saveResult.data).toEqual('replace with markdown')
|
||||
it 'saves xml from the xml editor', ->
|
||||
loadFixtures 'problem-without-markdown.html'
|
||||
@descriptor = new MarkdownEditingDescriptor($('.problem-editor'))
|
||||
saveResult = @descriptor.save()
|
||||
expect(saveResult.nullout).toEqual(['markdown'])
|
||||
expect(saveResult.data).toEqual('xml only')
|
||||
|
||||
describe 'advanced editor opens correctly', ->
|
||||
it 'click on advanced editor should work', ->
|
||||
loadFixtures 'problem-with-markdown.html'
|
||||
@descriptor = new MarkdownEditingDescriptor($('.problem-editor'))
|
||||
spyOn(@descriptor, 'confirmConversionToXml').and.returnValue(true)
|
||||
expect(@descriptor.confirmConversionToXml).not.toHaveBeenCalled()
|
||||
e = jasmine.createSpyObj('e', [ 'preventDefault' ])
|
||||
@descriptor.onShowXMLButton(e)
|
||||
expect(e.preventDefault).toHaveBeenCalled()
|
||||
expect(@descriptor.confirmConversionToXml).toHaveBeenCalled()
|
||||
expect($('.editor-bar').length).toEqual(0)
|
||||
|
||||
describe 'insertMultipleChoice', ->
|
||||
it 'inserts the template if selection is empty', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertMultipleChoice('')
|
||||
expect(revisedSelection).toEqual(MarkdownEditingDescriptor.multipleChoiceTemplate)
|
||||
it 'wraps existing text', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertMultipleChoice('foo\nbar')
|
||||
expect(revisedSelection).toEqual('( ) foo\n( ) bar\n')
|
||||
it 'recognizes x as a selection if there is non-whitespace after x', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertMultipleChoice('a\nx b\nc\nx \nd\n x e')
|
||||
expect(revisedSelection).toEqual('( ) a\n(x) b\n( ) c\n( ) x \n( ) d\n(x) e\n')
|
||||
it 'recognizes x as a selection if it is first non whitespace and has whitespace with other non-whitespace', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertMultipleChoice(' x correct\n x \nex post facto\nb x c\nx c\nxxp')
|
||||
expect(revisedSelection).toEqual('(x) correct\n( ) x \n( ) ex post facto\n( ) b x c\n(x) c\n( ) xxp\n')
|
||||
it 'removes multiple newlines but not last one', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertMultipleChoice('a\nx b\n\n\nc\n')
|
||||
expect(revisedSelection).toEqual('( ) a\n(x) b\n( ) c\n')
|
||||
|
||||
describe 'insertCheckboxChoice', ->
|
||||
# Note, shares code with insertMultipleChoice. Therefore only doing smoke test.
|
||||
it 'inserts the template if selection is empty', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertCheckboxChoice('')
|
||||
expect(revisedSelection).toEqual(MarkdownEditingDescriptor.checkboxChoiceTemplate)
|
||||
it 'wraps existing text', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertCheckboxChoice('foo\nbar')
|
||||
expect(revisedSelection).toEqual('[ ] foo\n[ ] bar\n')
|
||||
|
||||
describe 'insertStringInput', ->
|
||||
it 'inserts the template if selection is empty', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertStringInput('')
|
||||
expect(revisedSelection).toEqual(MarkdownEditingDescriptor.stringInputTemplate)
|
||||
it 'wraps existing text', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertStringInput('my text')
|
||||
expect(revisedSelection).toEqual('= my text')
|
||||
|
||||
describe 'insertNumberInput', ->
|
||||
it 'inserts the template if selection is empty', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertNumberInput('')
|
||||
expect(revisedSelection).toEqual(MarkdownEditingDescriptor.numberInputTemplate)
|
||||
it 'wraps existing text', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertNumberInput('my text')
|
||||
expect(revisedSelection).toEqual('= my text')
|
||||
|
||||
describe 'insertSelect', ->
|
||||
it 'inserts the template if selection is empty', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertSelect('')
|
||||
expect(revisedSelection).toEqual(MarkdownEditingDescriptor.selectTemplate)
|
||||
it 'wraps existing text', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertSelect('my text')
|
||||
expect(revisedSelection).toEqual('[[my text]]')
|
||||
|
||||
describe 'insertHeader', ->
|
||||
it 'inserts the template if selection is empty', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertHeader('')
|
||||
expect(revisedSelection).toEqual(MarkdownEditingDescriptor.headerTemplate)
|
||||
it 'wraps existing text', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertHeader('my text')
|
||||
expect(revisedSelection).toEqual('my text\n====\n')
|
||||
|
||||
describe 'insertExplanation', ->
|
||||
it 'inserts the template if selection is empty', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertExplanation('')
|
||||
expect(revisedSelection).toEqual(MarkdownEditingDescriptor.explanationTemplate)
|
||||
it 'wraps existing text', ->
|
||||
revisedSelection = MarkdownEditingDescriptor.insertExplanation('my text')
|
||||
expect(revisedSelection).toEqual('[explanation]\nmy text\n[explanation]')
|
||||
|
||||
describe 'markdownToXml', ->
|
||||
it 'converts raw text to paragraph', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml('foo')
|
||||
expect(data).toXMLEqual('<problem>\n <p>foo</p>\n</problem>')
|
||||
# test default templates
|
||||
it 'converts numerical response to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""A numerical response problem accepts a line of text input from the student, and evaluates the input for correctness based on its numerical value.
|
||||
|
||||
The answer is correct if it is within a specified numerical tolerance of the expected answer.
|
||||
|
||||
Enter the numerical value of Pi:
|
||||
= 3.14159 +- .02
|
||||
|
||||
Enter the approximate value of 502*9:
|
||||
= 502*9 +- 15%
|
||||
|
||||
Enter the number of fingers on a human hand:
|
||||
= 5
|
||||
|
||||
Range tolerance case
|
||||
= [6, 7]
|
||||
= (1, 2)
|
||||
|
||||
If first and last symbols are not brackets, or they are not closed, stringresponse will appear.
|
||||
= (7), 7
|
||||
= (1+2
|
||||
|
||||
[Explanation]
|
||||
Pi, or the the ratio between a circle's circumference to its diameter, is an irrational number known to extreme precision. It is value is approximately equal to 3.14.
|
||||
|
||||
Although you can get an exact value by typing 502*9 into a calculator, the result will be close to 500*10, or 5,000. The grader accepts any response within 15% of the true value, 4518, so that you can use any estimation technique that you like.
|
||||
|
||||
If you look at your hand, you can count that you have five fingers.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toXMLEqual("""<problem>
|
||||
<p>A numerical response problem accepts a line of text input from the student, and evaluates the input for correctness based on its numerical value.</p>
|
||||
<p>The answer is correct if it is within a specified numerical tolerance of the expected answer.</p>
|
||||
<p>Enter the numerical value of Pi:</p>
|
||||
<numericalresponse answer="3.14159">
|
||||
<responseparam type="tolerance" default=".02" />
|
||||
<formulaequationinput />
|
||||
</numericalresponse>
|
||||
<p>Enter the approximate value of 502*9:</p>
|
||||
<numericalresponse answer="502*9">
|
||||
<responseparam type="tolerance" default="15%" />
|
||||
<formulaequationinput />
|
||||
</numericalresponse>
|
||||
<p>Enter the number of fingers on a human hand:</p>
|
||||
<numericalresponse answer="5">
|
||||
<formulaequationinput />
|
||||
</numericalresponse>
|
||||
<p>Range tolerance case</p>
|
||||
<numericalresponse answer="[6, 7]">
|
||||
<formulaequationinput />
|
||||
</numericalresponse>
|
||||
<numericalresponse answer="(1, 2)">
|
||||
<formulaequationinput />
|
||||
</numericalresponse>
|
||||
<p>If first and last symbols are not brackets, or they are not closed, stringresponse will appear.</p>
|
||||
<stringresponse answer="(7), 7" type="ci" >
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
<stringresponse answer="(1+2" type="ci" >
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Pi, or the the ratio between a circle's circumference to its diameter, is an irrational number known to extreme precision. It is value is approximately equal to 3.14.</p>
|
||||
<p>Although you can get an exact value by typing 502*9 into a calculator, the result will be close to 500*10, or 5,000. The grader accepts any response within 15% of the true value, 4518, so that you can use any estimation technique that you like.</p>
|
||||
<p>If you look at your hand, you can count that you have five fingers.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</problem>""")
|
||||
it 'will convert 0 as a numerical response (instead of string response)', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
Enter 0 with a tolerance:
|
||||
= 0 +- .02
|
||||
""")
|
||||
expect(data).toXMLEqual("""<problem>
|
||||
<numericalresponse answer="0">
|
||||
<p>Enter 0 with a tolerance:</p>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
|
||||
|
||||
</problem>""")
|
||||
it 'markup with additional answer does not break numerical response', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
Enter 1 with a tolerance:
|
||||
= 1 +- .02
|
||||
or= 2
|
||||
""")
|
||||
expect(data).toXMLEqual("""<problem>
|
||||
<numericalresponse answer="1">
|
||||
<p>Enter 1 with a tolerance:</p>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<additional_answer answer="2"/>
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
|
||||
</problem>"""
|
||||
)
|
||||
it 'markup for numerical with multiple additional answers renders correctly', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
Enter 1 with a tolerance:
|
||||
= 1 +- .02
|
||||
or= 2
|
||||
or= 3
|
||||
""")
|
||||
expect(data).toXMLEqual("""<problem>
|
||||
<numericalresponse answer="1">
|
||||
<p>Enter 1 with a tolerance:</p>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<additional_answer answer="2"/>
|
||||
<additional_answer answer="3"/>
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
|
||||
</problem>"""
|
||||
)
|
||||
it 'Do not render ranged/tolerance/alphabetical additional answers for numerical response', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
Enter 1 with a tolerance:
|
||||
= 1 +- .02
|
||||
or= 2
|
||||
or= 3 +- 0.1
|
||||
or= [4,6]
|
||||
or= ABC
|
||||
or= 7
|
||||
""")
|
||||
expect(data).toXMLEqual("""<problem>
|
||||
<numericalresponse answer="1">
|
||||
<p>Enter 1 with a tolerance:</p>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<additional_answer answer="2"/>
|
||||
<additional_answer answer="7"/>
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
|
||||
</problem>"""
|
||||
)
|
||||
it 'markup with feedback renders correctly in additional answer for numerical response', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
Enter 1 with a tolerance:
|
||||
= 100 +- .02 {{ main feedback }}
|
||||
or= 10 {{ additional feedback }}
|
||||
""")
|
||||
expect(data).toXMLEqual("""<problem>
|
||||
<numericalresponse answer="100">
|
||||
<p>Enter 1 with a tolerance:</p>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<additional_answer answer="10">
|
||||
<correcthint>additional feedback</correcthint>
|
||||
</additional_answer>
|
||||
<formulaequationinput/>
|
||||
<correcthint>main feedback</correcthint>
|
||||
</numericalresponse>
|
||||
|
||||
</problem>"""
|
||||
)
|
||||
it 'converts multiple choice to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.
|
||||
|
||||
One of the main elements that goes into a good multiple choice question is the existence of good distractors. That is, each of the alternate responses presented to the student should be the result of a plausible mistake that a student might make.
|
||||
|
||||
>>What Apple device competed with the portable CD player?<<
|
||||
( ) The iPad
|
||||
( ) Napster
|
||||
(x) The iPod
|
||||
( ) The vegetable peeler
|
||||
( ) Android
|
||||
( ) The Beatles
|
||||
|
||||
[Explanation]
|
||||
The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toXMLEqual("""<problem>
|
||||
<multiplechoiceresponse>
|
||||
<p>A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.</p>
|
||||
<p>One of the main elements that goes into a good multiple choice question is the existence of good distractors. That is, each of the alternate responses presented to the student should be the result of a plausible mistake that a student might make.</p>
|
||||
<label>What Apple device competed with the portable CD player?</label>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">The iPad</choice>
|
||||
<choice correct="false">Napster</choice>
|
||||
<choice correct="true">The iPod</choice>
|
||||
<choice correct="false">The vegetable peeler</choice>
|
||||
<choice correct="false">Android</choice>
|
||||
<choice correct="false">The Beatles</choice>
|
||||
</choicegroup>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</multiplechoiceresponse>
|
||||
</problem>""")
|
||||
it 'converts multiple choice shuffle to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.
|
||||
|
||||
One of the main elements that goes into a good multiple choice question is the existence of good distractors. That is, each of the alternate responses presented to the student should be the result of a plausible mistake that a student might make.
|
||||
|
||||
What Apple device competed with the portable CD player?
|
||||
(!x@) The iPad
|
||||
(@) Napster
|
||||
() The iPod
|
||||
( ) The vegetable peeler
|
||||
( ) Android
|
||||
(@) The Beatles
|
||||
|
||||
[Explanation]
|
||||
The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<multiplechoiceresponse>
|
||||
<p>A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.</p>
|
||||
<p>One of the main elements that goes into a good multiple choice question is the existence of good distractors. That is, each of the alternate responses presented to the student should be the result of a plausible mistake that a student might make.</p>
|
||||
<p>What Apple device competed with the portable CD player?</p>
|
||||
<choicegroup type="MultipleChoice" shuffle="true">
|
||||
<choice correct="true" fixed="true">The iPad</choice>
|
||||
<choice correct="false" fixed="true">Napster</choice>
|
||||
<choice correct="false">The iPod</choice>
|
||||
<choice correct="false">The vegetable peeler</choice>
|
||||
<choice correct="false">Android</choice>
|
||||
<choice correct="false" fixed="true">The Beatles</choice>
|
||||
</choicegroup>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</multiplechoiceresponse>
|
||||
</problem>""")
|
||||
|
||||
it 'converts a series of multiplechoice to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""bleh
|
||||
(!x) a
|
||||
() b
|
||||
() c
|
||||
yatta
|
||||
( ) x
|
||||
( ) y
|
||||
(x) z
|
||||
testa
|
||||
(!) i
|
||||
( ) ii
|
||||
(x) iii
|
||||
[Explanation]
|
||||
When the student is ready, the explanation appears.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<p>bleh</p>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice" shuffle="true">
|
||||
<choice correct="true">a</choice>
|
||||
<choice correct="false">b</choice>
|
||||
<choice correct="false">c</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
<p>yatta</p>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">x</choice>
|
||||
<choice correct="false">y</choice>
|
||||
<choice correct="true">z</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
<p>testa</p>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice" shuffle="true">
|
||||
<choice correct="false">i</choice>
|
||||
<choice correct="false">ii</choice>
|
||||
<choice correct="true">iii</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>When the student is ready, the explanation appears.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</problem>""")
|
||||
|
||||
it 'converts OptionResponse to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""OptionResponse gives a limited set of options for students to respond with, and presents those options in a format that encourages them to search for a specific answer rather than being immediately presented with options from which to recognize the correct answer.
|
||||
|
||||
The answer options and the identification of the correct answer is defined in the <b>optioninput</b> tag.
|
||||
|
||||
Translation between Option Response and __________ is extremely straightforward:
|
||||
[[(Multiple Choice), String Response, Numerical Response, External Response, Image Response]]
|
||||
|
||||
[Explanation]
|
||||
Multiple Choice also allows students to select from a variety of pre-written responses, although the format makes it easier for students to read very long response options. Optionresponse also differs slightly because students are more likely to think of an answer and then search for it rather than relying purely on recognition to answer the question.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<optionresponse>
|
||||
<p>OptionResponse gives a limited set of options for students to respond with, and presents those options in a format that encourages them to search for a specific answer rather than being immediately presented with options from which to recognize the correct answer.</p>
|
||||
<p>The answer options and the identification of the correct answer is defined in the <b>optioninput</b> tag.</p>
|
||||
<p>Translation between Option Response and __________ is extremely straightforward:</p>
|
||||
<optioninput options="('Multiple Choice','String Response','Numerical Response','External Response','Image Response')" correct="Multiple Choice"/>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Multiple Choice also allows students to select from a variety of pre-written responses, although the format makes it easier for students to read very long response options. Optionresponse also differs slightly because students are more likely to think of an answer and then search for it rather than relying purely on recognition to answer the question.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</optionresponse>
|
||||
</problem>""")
|
||||
it 'converts StringResponse to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""A string response problem accepts a line of text input from the student, and evaluates the input for correctness based on an expected answer within each input box.
|
||||
|
||||
The answer is correct if it matches every character of the expected answer. This can be a problem with international spelling, dates, or anything where the format of the answer is not clear.
|
||||
|
||||
Which US state has Lansing as its capital?
|
||||
= Michigan
|
||||
|
||||
[Explanation]
|
||||
Lansing is the capital of Michigan, although it is not Michgan's largest city, or even the seat of the county in which it resides.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<stringresponse answer="Michigan" type="ci">
|
||||
<p>A string response problem accepts a line of text input from the student, and evaluates the input for correctness based on an expected answer within each input box.</p>
|
||||
<p>The answer is correct if it matches every character of the expected answer. This can be a problem with international spelling, dates, or anything where the format of the answer is not clear.</p>
|
||||
<p>Which US state has Lansing as its capital?</p>
|
||||
<textline size="20"/>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Lansing is the capital of Michigan, although it is not Michgan's largest city, or even the seat of the county in which it resides.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</stringresponse>
|
||||
</problem>""")
|
||||
it 'converts StringResponse with regular expression to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""Who lead the civil right movement in the United States of America?
|
||||
= | \w*\.?\s*Luther King\s*.*
|
||||
|
||||
[Explanation]
|
||||
Test Explanation.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<stringresponse answer="w*.?s*Luther Kings*.*" type="ci regexp">
|
||||
<p>Who lead the civil right movement in the United States of America?</p>
|
||||
<textline size="20"/>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Test Explanation.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</stringresponse>
|
||||
</problem>""")
|
||||
it 'converts StringResponse with multiple answers to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""Who lead the civil right movement in the United States of America?
|
||||
= Dr. Martin Luther King Jr.
|
||||
or= Doctor Martin Luther King Junior
|
||||
or= Martin Luther King
|
||||
or= Martin Luther King Junior
|
||||
|
||||
[Explanation]
|
||||
Test Explanation.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<stringresponse answer="Dr. Martin Luther King Jr." type="ci">
|
||||
<p>Who lead the civil right movement in the United States of America?</p>
|
||||
<additional_answer answer="Doctor Martin Luther King Junior"/>
|
||||
<additional_answer answer="Martin Luther King"/>
|
||||
<additional_answer answer="Martin Luther King Junior"/>
|
||||
<textline size="20"/>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Test Explanation.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</stringresponse>
|
||||
</problem>""")
|
||||
it 'converts StringResponse with multiple answers and regular expressions to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""Write a number from 1 to 4.
|
||||
=| ^One$
|
||||
or= two
|
||||
or= ^thre+
|
||||
or= ^4|Four$
|
||||
|
||||
[Explanation]
|
||||
Test Explanation.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<stringresponse answer="^One$" type="ci regexp">
|
||||
<p>Write a number from 1 to 4.</p>
|
||||
<additional_answer answer="two"/>
|
||||
<additional_answer answer="^thre+"/>
|
||||
<additional_answer answer="^4|Four$"/>
|
||||
<textline size="20"/>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Test Explanation.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</stringresponse>
|
||||
</problem>""")
|
||||
# test labels
|
||||
it 'converts markdown labels to label attributes', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml(""">>Who lead the civil right movement in the United States of America?<<
|
||||
= | \w*\.?\s*Luther King\s*.*
|
||||
|
||||
[Explanation]
|
||||
Test Explanation.
|
||||
[Explanation]
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<stringresponse answer="w*.?s*Luther Kings*.*" type="ci regexp">
|
||||
<label>Who lead the civil right movement in the United States of America?</label>
|
||||
<textline size="20"/>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Test Explanation.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</stringresponse>
|
||||
</problem>""")
|
||||
it 'handles multiple questions with labels', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
France is a country in Europe.
|
||||
|
||||
>>What is the capital of France?<<
|
||||
= Paris
|
||||
|
||||
Germany is a country in Europe, too.
|
||||
|
||||
>>What is the capital of Germany?<<
|
||||
( ) Bonn
|
||||
( ) Hamburg
|
||||
(x) Berlin
|
||||
( ) Donut
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<p>France is a country in Europe.</p>
|
||||
|
||||
<label>What is the capital of France?</label>
|
||||
<stringresponse answer="Paris" type="ci" >
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
<p>Germany is a country in Europe, too.</p>
|
||||
|
||||
<label>What is the capital of Germany?</label>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Bonn</choice>
|
||||
<choice correct="false">Hamburg</choice>
|
||||
<choice correct="true">Berlin</choice>
|
||||
<choice correct="false">Donut</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
</problem>""")
|
||||
it 'tests multiple questions with only one label', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
France is a country in Europe.
|
||||
|
||||
>>What is the capital of France?<<
|
||||
= Paris
|
||||
|
||||
Germany is a country in Europe, too.
|
||||
|
||||
What is the capital of Germany?
|
||||
( ) Bonn
|
||||
( ) Hamburg
|
||||
(x) Berlin
|
||||
( ) Donut
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<p>France is a country in Europe.</p>
|
||||
|
||||
<label>What is the capital of France?</label>
|
||||
<stringresponse answer="Paris" type="ci" >
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
<p>Germany is a country in Europe, too.</p>
|
||||
|
||||
<p>What is the capital of Germany?</p>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Bonn</choice>
|
||||
<choice correct="false">Hamburg</choice>
|
||||
<choice correct="true">Berlin</choice>
|
||||
<choice correct="false">Donut</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
</problem>""")
|
||||
|
||||
it 'adds labels to formulae', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>Enter the numerical value of Pi:<<
|
||||
= 3.14159 +- .02
|
||||
""")
|
||||
expect(data).toXMLEqual("""<problem>
|
||||
<numericalresponse answer="3.14159">
|
||||
<label>Enter the numerical value of Pi:</label>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
|
||||
|
||||
</problem>""")
|
||||
|
||||
# test oddities
|
||||
it 'converts headers and oddities to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""Not a header
|
||||
A header
|
||||
==============
|
||||
|
||||
Multiple choice w/ parentheticals
|
||||
( ) option (with parens)
|
||||
( ) xd option (x)
|
||||
()) parentheses inside
|
||||
() no space b4 close paren
|
||||
|
||||
Choice checks
|
||||
[ ] option1 [x]
|
||||
[x] correct
|
||||
[x] redundant
|
||||
[(] distractor
|
||||
[] no space
|
||||
|
||||
Option with multiple correct ones
|
||||
[[one option, (correct one), (should not be correct)]]
|
||||
|
||||
Option with embedded parens
|
||||
[[My (heart), another, (correct)]]
|
||||
|
||||
What happens w/ empty correct options?
|
||||
[[()]]
|
||||
|
||||
[Explanation]see[/expLanation]
|
||||
|
||||
[explanation]
|
||||
orphaned start
|
||||
|
||||
No p tags in the below
|
||||
<script type='javascript'>
|
||||
var two = 2;
|
||||
|
||||
console.log(two * 2);
|
||||
</script>
|
||||
|
||||
But in this there should be
|
||||
<div>
|
||||
Great ideas require offsetting.
|
||||
|
||||
bad tests require drivel
|
||||
</div>
|
||||
|
||||
[code]
|
||||
Code should be nicely monospaced.
|
||||
[/code]
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<p>Not a header</p>
|
||||
<h3 class="hd hd-2 problem-header">A header</h3>
|
||||
<p>Multiple choice w/ parentheticals</p>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">option (with parens)</choice>
|
||||
<choice correct="false">xd option (x)</choice>
|
||||
<choice correct="false">parentheses inside</choice>
|
||||
<choice correct="false">no space b4 close paren</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
<p>Choice checks</p>
|
||||
<choiceresponse>
|
||||
<checkboxgroup>
|
||||
<choice correct="false">option1 [x]</choice>
|
||||
<choice correct="true">correct</choice>
|
||||
<choice correct="true">redundant</choice>
|
||||
<choice correct="false">distractor</choice>
|
||||
<choice correct="false">no space</choice>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
<p>Option with multiple correct ones</p>
|
||||
<optionresponse>
|
||||
<optioninput options="('one option','correct one','should not be correct')" correct="correct one"></optioninput>
|
||||
</optionresponse>
|
||||
<p>Option with embedded parens</p>
|
||||
<optionresponse>
|
||||
<optioninput options="('My (heart)','another','correct')" correct="correct"></optioninput>
|
||||
</optionresponse>
|
||||
<p>What happens w/ empty correct options?</p>
|
||||
<optionresponse>
|
||||
<optioninput options="('')" correct=""></optioninput>
|
||||
</optionresponse>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>see</p>
|
||||
</div>
|
||||
</solution>
|
||||
<p>[explanation]</p>
|
||||
<p>orphaned start</p>
|
||||
<p>No p tags in the below</p>
|
||||
<script type='javascript'>
|
||||
var two = 2;
|
||||
|
||||
console.log(two * 2);
|
||||
</script>
|
||||
<p>But in this there should be</p>
|
||||
<div>
|
||||
<p>Great ideas require offsetting.</p>
|
||||
<p>bad tests require drivel</p>
|
||||
</div>
|
||||
<pre>
|
||||
<code>Code should be nicely monospaced.
|
||||
</code>
|
||||
</pre>
|
||||
</problem>""")
|
||||
|
||||
it 'can separate responsetypes based on ---', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
Multiple choice problems allow learners to select only one option. Learners can see all the options along with the problem text.
|
||||
|
||||
>>Which of the following countries has the largest population?<<
|
||||
( ) Brazil {{ timely feedback -- explain why an almost correct answer is wrong }}
|
||||
( ) Germany
|
||||
(x) Indonesia
|
||||
( ) Russia
|
||||
|
||||
[explanation]
|
||||
According to September 2014 estimates:
|
||||
The population of Indonesia is approximately 250 million.
|
||||
The population of Brazil is approximately 200 million.
|
||||
The population of Russia is approximately 146 million.
|
||||
The population of Germany is approximately 81 million.
|
||||
[explanation]
|
||||
|
||||
---
|
||||
|
||||
Checkbox problems allow learners to select multiple options. Learners can see all the options along with the problem text.
|
||||
|
||||
>>The following languages are in the Indo-European family:<<
|
||||
[x] Urdu
|
||||
[ ] Finnish
|
||||
[x] Marathi
|
||||
[x] French
|
||||
[ ] Hungarian
|
||||
|
||||
Note: Make sure you select all of the correct options—there may be more than one!
|
||||
|
||||
[explanation]
|
||||
Urdu, Marathi, and French are all Indo-European languages, while Finnish and Hungarian are in the Uralic family.
|
||||
[explanation]
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<multiplechoiceresponse>
|
||||
<p>Multiple choice problems allow learners to select only one option. Learners can see all the options along with the problem text.</p>
|
||||
<label>Which of the following countries has the largest population?</label>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Brazil <choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Germany</choice>
|
||||
<choice correct="true">Indonesia</choice>
|
||||
<choice correct="false">Russia</choice>
|
||||
</choicegroup>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>According to September 2014 estimates:</p>
|
||||
<p>The population of Indonesia is approximately 250 million.</p>
|
||||
<p>The population of Brazil is approximately 200 million.</p>
|
||||
<p>The population of Russia is approximately 146 million.</p>
|
||||
<p>The population of Germany is approximately 81 million.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<choiceresponse>
|
||||
<p>Checkbox problems allow learners to select multiple options. Learners can see all the options along with the problem text.</p>
|
||||
<label>The following languages are in the Indo-European family:</label>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">Urdu</choice>
|
||||
<choice correct="false">Finnish</choice>
|
||||
<choice correct="true">Marathi</choice>
|
||||
<choice correct="true">French</choice>
|
||||
<choice correct="false">Hungarian</choice>
|
||||
</checkboxgroup>
|
||||
<p>Note: Make sure you select all of the correct options—there may be more than one!</p>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Urdu, Marathi, and French are all Indo-European languages, while Finnish and Hungarian are in the Uralic family.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</choiceresponse>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'can separate other things based on ---', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
Multiple choice problems allow learners to select only one option. Learners can see all the options along with the problem text.
|
||||
|
||||
---
|
||||
|
||||
>>Which of the following countries has the largest population?<<
|
||||
( ) Brazil {{ timely feedback -- explain why an almost correct answer is wrong }}
|
||||
( ) Germany
|
||||
(x) Indonesia
|
||||
( ) Russia
|
||||
|
||||
[explanation]
|
||||
According to September 2014 estimates:
|
||||
The population of Indonesia is approximately 250 million.
|
||||
The population of Brazil is approximately 200 million.
|
||||
The population of Russia is approximately 146 million.
|
||||
The population of Germany is approximately 81 million.
|
||||
[explanation]
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<p>Multiple choice problems allow learners to select only one option. Learners can see all the options along with the problem text.</p>
|
||||
|
||||
<multiplechoiceresponse>
|
||||
<label>Which of the following countries has the largest population?</label>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Brazil <choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Germany</choice>
|
||||
<choice correct="true">Indonesia</choice>
|
||||
<choice correct="false">Russia</choice>
|
||||
</choicegroup>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>According to September 2014 estimates:</p>
|
||||
<p>The population of Indonesia is approximately 250 million.</p>
|
||||
<p>The population of Brazil is approximately 200 million.</p>
|
||||
<p>The population of Russia is approximately 146 million.</p>
|
||||
<p>The population of Germany is approximately 81 million.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</multiplechoiceresponse>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'can do separation if spaces are present around ---', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>The following languages are in the Indo-European family:||There are three correct choices.<<
|
||||
[x] Urdu
|
||||
[ ] Finnish
|
||||
[x] Marathi
|
||||
[x] French
|
||||
[ ] Hungarian
|
||||
|
||||
---
|
||||
|
||||
>>Which of the following countries has the largest population?||You have only choice.<<
|
||||
( ) Brazil {{ timely feedback -- explain why an almost correct answer is wrong }}
|
||||
( ) Germany
|
||||
(x) Indonesia
|
||||
( ) Russia
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<choiceresponse>
|
||||
<label>The following languages are in the Indo-European family:</label>
|
||||
<description>There are three correct choices.</description>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">Urdu</choice>
|
||||
<choice correct="false">Finnish</choice>
|
||||
<choice correct="true">Marathi</choice>
|
||||
<choice correct="true">French</choice>
|
||||
<choice correct="false">Hungarian</choice>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
|
||||
<multiplechoiceresponse>
|
||||
<label>Which of the following countries has the largest population?</label>
|
||||
<description>You have only choice.</description>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Brazil
|
||||
<choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Germany</choice>
|
||||
<choice correct="true">Indonesia</choice>
|
||||
<choice correct="false">Russia</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'can extract question description', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>The following languages are in the Indo-European family:||Choose wisely.<<
|
||||
[x] Urdu
|
||||
[ ] Finnish
|
||||
[x] Marathi
|
||||
[x] French
|
||||
[ ] Hungarian
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<choiceresponse>
|
||||
<label>The following languages are in the Indo-European family:</label>
|
||||
<description>Choose wisely.</description>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">Urdu</choice>
|
||||
<choice correct="false">Finnish</choice>
|
||||
<choice correct="true">Marathi</choice>
|
||||
<choice correct="true">French</choice>
|
||||
<choice correct="false">Hungarian</choice>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'can handle question and description spanned across multiple lines', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>The following languages
|
||||
are in the
|
||||
Indo-European family:
|
||||
||
|
||||
first second
|
||||
third
|
||||
<<
|
||||
[x] Urdu
|
||||
[ ] Finnish
|
||||
[x] Marathi
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<choiceresponse>
|
||||
<label>The following languages are in the Indo-European family:</label>
|
||||
<description>first second third</description>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">Urdu</choice>
|
||||
<choice correct="false">Finnish</choice>
|
||||
<choice correct="true">Marathi</choice>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'will not add empty description', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>The following languages are in the Indo-European family:||<<
|
||||
[x] Urdu
|
||||
[ ] Finnish
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<choiceresponse>
|
||||
<label>The following languages are in the Indo-European family:</label>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">Urdu</choice>
|
||||
<choice correct="false">Finnish</choice>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
</problem>
|
||||
""")
|
||||
1043
common/lib/xmodule/xmodule/js/spec/problem/edit_spec.js
Normal file
1043
common/lib/xmodule/xmodule/js/spec/problem/edit_spec.js
Normal file
@@ -0,0 +1,1043 @@
|
||||
describe('MarkdownEditingDescriptor', function() {
|
||||
describe('save stores the correct data', function() {
|
||||
it('saves markdown from markdown editor', function() {
|
||||
loadFixtures('problem-with-markdown.html');
|
||||
this.descriptor = new MarkdownEditingDescriptor($('.problem-editor'));
|
||||
const saveResult = this.descriptor.save();
|
||||
expect(saveResult.metadata.markdown).toEqual('markdown');
|
||||
expect(saveResult.data).toXMLEqual('<problem>\n <p>markdown</p>\n</problem>');
|
||||
});
|
||||
it('clears markdown when xml editor is selected', function() {
|
||||
loadFixtures('problem-with-markdown.html');
|
||||
this.descriptor = new MarkdownEditingDescriptor($('.problem-editor'));
|
||||
this.descriptor.createXMLEditor('replace with markdown');
|
||||
const saveResult = this.descriptor.save();
|
||||
expect(saveResult.nullout).toEqual(['markdown']);
|
||||
expect(saveResult.data).toEqual('replace with markdown');
|
||||
});
|
||||
it('saves xml from the xml editor', function() {
|
||||
loadFixtures('problem-without-markdown.html');
|
||||
this.descriptor = new MarkdownEditingDescriptor($('.problem-editor'));
|
||||
const saveResult = this.descriptor.save();
|
||||
expect(saveResult.nullout).toEqual(['markdown']);
|
||||
expect(saveResult.data).toEqual('xml only');
|
||||
});
|
||||
});
|
||||
|
||||
describe('advanced editor opens correctly', () =>
|
||||
it('click on advanced editor should work', function() {
|
||||
loadFixtures('problem-with-markdown.html');
|
||||
this.descriptor = new MarkdownEditingDescriptor($('.problem-editor'));
|
||||
spyOn(this.descriptor, 'confirmConversionToXml').and.returnValue(true);
|
||||
expect(this.descriptor.confirmConversionToXml).not.toHaveBeenCalled();
|
||||
const e = jasmine.createSpyObj('e', [ 'preventDefault' ]);
|
||||
this.descriptor.onShowXMLButton(e);
|
||||
expect(e.preventDefault).toHaveBeenCalled();
|
||||
expect(this.descriptor.confirmConversionToXml).toHaveBeenCalled();
|
||||
expect($('.editor-bar').length).toEqual(0);
|
||||
})
|
||||
);
|
||||
|
||||
describe('insertMultipleChoice', function() {
|
||||
it('inserts the template if selection is empty', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertMultipleChoice('');
|
||||
expect(revisedSelection).toEqual(MarkdownEditingDescriptor.multipleChoiceTemplate);
|
||||
});
|
||||
it('wraps existing text', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertMultipleChoice('foo\nbar');
|
||||
expect(revisedSelection).toEqual('( ) foo\n( ) bar\n');
|
||||
});
|
||||
it('recognizes x as a selection if there is non-whitespace after x', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertMultipleChoice('a\nx b\nc\nx \nd\n x e');
|
||||
expect(revisedSelection).toEqual('( ) a\n(x) b\n( ) c\n( ) x \n( ) d\n(x) e\n');
|
||||
});
|
||||
it('recognizes x as a selection if it is first non whitespace and has whitespace with other non-whitespace', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertMultipleChoice(' x correct\n x \nex post facto\nb x c\nx c\nxxp');
|
||||
expect(revisedSelection).toEqual('(x) correct\n( ) x \n( ) ex post facto\n( ) b x c\n(x) c\n( ) xxp\n');
|
||||
});
|
||||
it('removes multiple newlines but not last one', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertMultipleChoice('a\nx b\n\n\nc\n');
|
||||
expect(revisedSelection).toEqual('( ) a\n(x) b\n( ) c\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertCheckboxChoice', function() {
|
||||
// Note, shares code with insertMultipleChoice. Therefore only doing smoke test.
|
||||
it('inserts the template if selection is empty', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertCheckboxChoice('');
|
||||
expect(revisedSelection).toEqual(MarkdownEditingDescriptor.checkboxChoiceTemplate);
|
||||
});
|
||||
it('wraps existing text', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertCheckboxChoice('foo\nbar');
|
||||
expect(revisedSelection).toEqual('[ ] foo\n[ ] bar\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertStringInput', function() {
|
||||
it('inserts the template if selection is empty', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertStringInput('');
|
||||
expect(revisedSelection).toEqual(MarkdownEditingDescriptor.stringInputTemplate);
|
||||
});
|
||||
it('wraps existing text', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertStringInput('my text');
|
||||
expect(revisedSelection).toEqual('= my text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertNumberInput', function() {
|
||||
it('inserts the template if selection is empty', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertNumberInput('');
|
||||
expect(revisedSelection).toEqual(MarkdownEditingDescriptor.numberInputTemplate);
|
||||
});
|
||||
it('wraps existing text', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertNumberInput('my text');
|
||||
expect(revisedSelection).toEqual('= my text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertSelect', function() {
|
||||
it('inserts the template if selection is empty', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertSelect('');
|
||||
expect(revisedSelection).toEqual(MarkdownEditingDescriptor.selectTemplate);
|
||||
});
|
||||
it('wraps existing text', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertSelect('my text');
|
||||
expect(revisedSelection).toEqual('[[my text]]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertHeader', function() {
|
||||
it('inserts the template if selection is empty', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertHeader('');
|
||||
expect(revisedSelection).toEqual(MarkdownEditingDescriptor.headerTemplate);
|
||||
});
|
||||
it('wraps existing text', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertHeader('my text');
|
||||
expect(revisedSelection).toEqual('my text\n====\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertExplanation', function() {
|
||||
it('inserts the template if selection is empty', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertExplanation('');
|
||||
expect(revisedSelection).toEqual(MarkdownEditingDescriptor.explanationTemplate);
|
||||
});
|
||||
it('wraps existing text', function() {
|
||||
const revisedSelection = MarkdownEditingDescriptor.insertExplanation('my text');
|
||||
expect(revisedSelection).toEqual('[explanation]\nmy text\n[explanation]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('markdownToXml', function() {
|
||||
it('converts raw text to paragraph', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml('foo');
|
||||
expect(data).toXMLEqual('<problem>\n <p>foo</p>\n</problem>');
|
||||
});
|
||||
// test default templates
|
||||
it('converts numerical response to xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`A numerical response problem accepts a line of text input from the student, and evaluates the input for correctness based on its numerical value.
|
||||
|
||||
The answer is correct if it is within a specified numerical tolerance of the expected answer.
|
||||
|
||||
Enter the numerical value of Pi:
|
||||
= 3.14159 +- .02
|
||||
|
||||
Enter the approximate value of 502*9:
|
||||
= 502*9 +- 15%
|
||||
|
||||
Enter the number of fingers on a human hand:
|
||||
= 5
|
||||
|
||||
Range tolerance case
|
||||
= [6, 7]
|
||||
= (1, 2)
|
||||
|
||||
If first and last symbols are not brackets, or they are not closed, stringresponse will appear.
|
||||
= (7), 7
|
||||
= (1+2
|
||||
|
||||
[Explanation]
|
||||
Pi, or the the ratio between a circle's circumference to its diameter, is an irrational number known to extreme precision. It is value is approximately equal to 3.14.
|
||||
|
||||
Although you can get an exact value by typing 502*9 into a calculator, the result will be close to 500*10, or 5,000. The grader accepts any response within 15% of the true value, 4518, so that you can use any estimation technique that you like.
|
||||
|
||||
If you look at your hand, you can count that you have five fingers.
|
||||
[Explanation]\
|
||||
`);
|
||||
expect(data).toXMLEqual(`<problem>
|
||||
<p>A numerical response problem accepts a line of text input from the student, and evaluates the input for correctness based on its numerical value.</p>
|
||||
<p>The answer is correct if it is within a specified numerical tolerance of the expected answer.</p>
|
||||
<p>Enter the numerical value of Pi:</p>
|
||||
<numericalresponse answer="3.14159">
|
||||
<responseparam type="tolerance" default=".02" />
|
||||
<formulaequationinput />
|
||||
</numericalresponse>
|
||||
<p>Enter the approximate value of 502*9:</p>
|
||||
<numericalresponse answer="502*9">
|
||||
<responseparam type="tolerance" default="15%" />
|
||||
<formulaequationinput />
|
||||
</numericalresponse>
|
||||
<p>Enter the number of fingers on a human hand:</p>
|
||||
<numericalresponse answer="5">
|
||||
<formulaequationinput />
|
||||
</numericalresponse>
|
||||
<p>Range tolerance case</p>
|
||||
<numericalresponse answer="[6, 7]">
|
||||
<formulaequationinput />
|
||||
</numericalresponse>
|
||||
<numericalresponse answer="(1, 2)">
|
||||
<formulaequationinput />
|
||||
</numericalresponse>
|
||||
<p>If first and last symbols are not brackets, or they are not closed, stringresponse will appear.</p>
|
||||
<stringresponse answer="(7), 7" type="ci" >
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
<stringresponse answer="(1+2" type="ci" >
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Pi, or the the ratio between a circle's circumference to its diameter, is an irrational number known to extreme precision. It is value is approximately equal to 3.14.</p>
|
||||
<p>Although you can get an exact value by typing 502*9 into a calculator, the result will be close to 500*10, or 5,000. The grader accepts any response within 15% of the true value, 4518, so that you can use any estimation technique that you like.</p>
|
||||
<p>If you look at your hand, you can count that you have five fingers.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</problem>`);
|
||||
});
|
||||
it('will convert 0 as a numerical response (instead of string response)', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
Enter 0 with a tolerance:
|
||||
= 0 +- .02\
|
||||
`);
|
||||
expect(data).toXMLEqual(`<problem>
|
||||
<numericalresponse answer="0">
|
||||
<p>Enter 0 with a tolerance:</p>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
|
||||
|
||||
</problem>`);
|
||||
});
|
||||
it('markup with additional answer does not break numerical response', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
Enter 1 with a tolerance:
|
||||
= 1 +- .02
|
||||
or= 2\
|
||||
`);
|
||||
expect(data).toXMLEqual(`<problem>
|
||||
<numericalresponse answer="1">
|
||||
<p>Enter 1 with a tolerance:</p>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<additional_answer answer="2"/>
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
|
||||
</problem>`
|
||||
);
|
||||
});
|
||||
it('markup for numerical with multiple additional answers renders correctly', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
Enter 1 with a tolerance:
|
||||
= 1 +- .02
|
||||
or= 2
|
||||
or= 3\
|
||||
`);
|
||||
expect(data).toXMLEqual(`<problem>
|
||||
<numericalresponse answer="1">
|
||||
<p>Enter 1 with a tolerance:</p>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<additional_answer answer="2"/>
|
||||
<additional_answer answer="3"/>
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
|
||||
</problem>`
|
||||
);
|
||||
});
|
||||
it('Do not render ranged/tolerance/alphabetical additional answers for numerical response', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
Enter 1 with a tolerance:
|
||||
= 1 +- .02
|
||||
or= 2
|
||||
or= 3 +- 0.1
|
||||
or= [4,6]
|
||||
or= ABC
|
||||
or= 7\
|
||||
`);
|
||||
expect(data).toXMLEqual(`<problem>
|
||||
<numericalresponse answer="1">
|
||||
<p>Enter 1 with a tolerance:</p>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<additional_answer answer="2"/>
|
||||
<additional_answer answer="7"/>
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
|
||||
</problem>`
|
||||
);
|
||||
});
|
||||
it('markup with feedback renders correctly in additional answer for numerical response', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
Enter 1 with a tolerance:
|
||||
= 100 +- .02 {{ main feedback }}
|
||||
or= 10 {{ additional feedback }}\
|
||||
`);
|
||||
expect(data).toXMLEqual(`<problem>
|
||||
<numericalresponse answer="100">
|
||||
<p>Enter 1 with a tolerance:</p>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<additional_answer answer="10">
|
||||
<correcthint>additional feedback</correcthint>
|
||||
</additional_answer>
|
||||
<formulaequationinput/>
|
||||
<correcthint>main feedback</correcthint>
|
||||
</numericalresponse>
|
||||
|
||||
</problem>`
|
||||
);
|
||||
});
|
||||
it('converts multiple choice to xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.
|
||||
|
||||
One of the main elements that goes into a good multiple choice question is the existence of good distractors. That is, each of the alternate responses presented to the student should be the result of a plausible mistake that a student might make.
|
||||
|
||||
>>What Apple device competed with the portable CD player?<<
|
||||
( ) The iPad
|
||||
( ) Napster
|
||||
(x) The iPod
|
||||
( ) The vegetable peeler
|
||||
( ) Android
|
||||
( ) The Beatles
|
||||
|
||||
[Explanation]
|
||||
The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks.
|
||||
[Explanation]\
|
||||
`);
|
||||
expect(data).toXMLEqual(`<problem>
|
||||
<multiplechoiceresponse>
|
||||
<p>A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.</p>
|
||||
<p>One of the main elements that goes into a good multiple choice question is the existence of good distractors. That is, each of the alternate responses presented to the student should be the result of a plausible mistake that a student might make.</p>
|
||||
<label>What Apple device competed with the portable CD player?</label>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">The iPad</choice>
|
||||
<choice correct="false">Napster</choice>
|
||||
<choice correct="true">The iPod</choice>
|
||||
<choice correct="false">The vegetable peeler</choice>
|
||||
<choice correct="false">Android</choice>
|
||||
<choice correct="false">The Beatles</choice>
|
||||
</choicegroup>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</multiplechoiceresponse>
|
||||
</problem>`);
|
||||
});
|
||||
it('converts multiple choice shuffle to xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.
|
||||
|
||||
One of the main elements that goes into a good multiple choice question is the existence of good distractors. That is, each of the alternate responses presented to the student should be the result of a plausible mistake that a student might make.
|
||||
|
||||
What Apple device competed with the portable CD player?
|
||||
(!x@) The iPad
|
||||
(@) Napster
|
||||
() The iPod
|
||||
( ) The vegetable peeler
|
||||
( ) Android
|
||||
(@) The Beatles
|
||||
|
||||
[Explanation]
|
||||
The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks.
|
||||
[Explanation]\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<multiplechoiceresponse>
|
||||
<p>A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.</p>
|
||||
<p>One of the main elements that goes into a good multiple choice question is the existence of good distractors. That is, each of the alternate responses presented to the student should be the result of a plausible mistake that a student might make.</p>
|
||||
<p>What Apple device competed with the portable CD player?</p>
|
||||
<choicegroup type="MultipleChoice" shuffle="true">
|
||||
<choice correct="true" fixed="true">The iPad</choice>
|
||||
<choice correct="false" fixed="true">Napster</choice>
|
||||
<choice correct="false">The iPod</choice>
|
||||
<choice correct="false">The vegetable peeler</choice>
|
||||
<choice correct="false">Android</choice>
|
||||
<choice correct="false" fixed="true">The Beatles</choice>
|
||||
</choicegroup>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</multiplechoiceresponse>
|
||||
</problem>`);
|
||||
});
|
||||
|
||||
it('converts a series of multiplechoice to xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`bleh
|
||||
(!x) a
|
||||
() b
|
||||
() c
|
||||
yatta
|
||||
( ) x
|
||||
( ) y
|
||||
(x) z
|
||||
testa
|
||||
(!) i
|
||||
( ) ii
|
||||
(x) iii
|
||||
[Explanation]
|
||||
When the student is ready, the explanation appears.
|
||||
[Explanation]\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<p>bleh</p>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice" shuffle="true">
|
||||
<choice correct="true">a</choice>
|
||||
<choice correct="false">b</choice>
|
||||
<choice correct="false">c</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
<p>yatta</p>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">x</choice>
|
||||
<choice correct="false">y</choice>
|
||||
<choice correct="true">z</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
<p>testa</p>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice" shuffle="true">
|
||||
<choice correct="false">i</choice>
|
||||
<choice correct="false">ii</choice>
|
||||
<choice correct="true">iii</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>When the student is ready, the explanation appears.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</problem>`);
|
||||
});
|
||||
|
||||
it('converts OptionResponse to xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`OptionResponse gives a limited set of options for students to respond with, and presents those options in a format that encourages them to search for a specific answer rather than being immediately presented with options from which to recognize the correct answer.
|
||||
|
||||
The answer options and the identification of the correct answer is defined in the <b>optioninput</b> tag.
|
||||
|
||||
Translation between Option Response and __________ is extremely straightforward:
|
||||
[[(Multiple Choice), String Response, Numerical Response, External Response, Image Response]]
|
||||
|
||||
[Explanation]
|
||||
Multiple Choice also allows students to select from a variety of pre-written responses, although the format makes it easier for students to read very long response options. Optionresponse also differs slightly because students are more likely to think of an answer and then search for it rather than relying purely on recognition to answer the question.
|
||||
[Explanation]\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<optionresponse>
|
||||
<p>OptionResponse gives a limited set of options for students to respond with, and presents those options in a format that encourages them to search for a specific answer rather than being immediately presented with options from which to recognize the correct answer.</p>
|
||||
<p>The answer options and the identification of the correct answer is defined in the <b>optioninput</b> tag.</p>
|
||||
<p>Translation between Option Response and __________ is extremely straightforward:</p>
|
||||
<optioninput options="('Multiple Choice','String Response','Numerical Response','External Response','Image Response')" correct="Multiple Choice"/>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Multiple Choice also allows students to select from a variety of pre-written responses, although the format makes it easier for students to read very long response options. Optionresponse also differs slightly because students are more likely to think of an answer and then search for it rather than relying purely on recognition to answer the question.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</optionresponse>
|
||||
</problem>`);
|
||||
});
|
||||
it('converts StringResponse to xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`A string response problem accepts a line of text input from the student, and evaluates the input for correctness based on an expected answer within each input box.
|
||||
|
||||
The answer is correct if it matches every character of the expected answer. This can be a problem with international spelling, dates, or anything where the format of the answer is not clear.
|
||||
|
||||
Which US state has Lansing as its capital?
|
||||
= Michigan
|
||||
|
||||
[Explanation]
|
||||
Lansing is the capital of Michigan, although it is not Michgan's largest city, or even the seat of the county in which it resides.
|
||||
[Explanation]\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<stringresponse answer="Michigan" type="ci">
|
||||
<p>A string response problem accepts a line of text input from the student, and evaluates the input for correctness based on an expected answer within each input box.</p>
|
||||
<p>The answer is correct if it matches every character of the expected answer. This can be a problem with international spelling, dates, or anything where the format of the answer is not clear.</p>
|
||||
<p>Which US state has Lansing as its capital?</p>
|
||||
<textline size="20"/>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Lansing is the capital of Michigan, although it is not Michgan's largest city, or even the seat of the county in which it resides.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</stringresponse>
|
||||
</problem>`);
|
||||
});
|
||||
it('converts StringResponse with regular expression to xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`Who lead the civil right movement in the United States of America?
|
||||
= | \w*\.?\s*Luther King\s*.*
|
||||
|
||||
[Explanation]
|
||||
Test Explanation.
|
||||
[Explanation]\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<stringresponse answer="w*.?s*Luther Kings*.*" type="ci regexp">
|
||||
<p>Who lead the civil right movement in the United States of America?</p>
|
||||
<textline size="20"/>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Test Explanation.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</stringresponse>
|
||||
</problem>`);
|
||||
});
|
||||
it('converts StringResponse with multiple answers to xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`Who lead the civil right movement in the United States of America?
|
||||
= Dr. Martin Luther King Jr.
|
||||
or= Doctor Martin Luther King Junior
|
||||
or= Martin Luther King
|
||||
or= Martin Luther King Junior
|
||||
|
||||
[Explanation]
|
||||
Test Explanation.
|
||||
[Explanation]\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<stringresponse answer="Dr. Martin Luther King Jr." type="ci">
|
||||
<p>Who lead the civil right movement in the United States of America?</p>
|
||||
<additional_answer answer="Doctor Martin Luther King Junior"/>
|
||||
<additional_answer answer="Martin Luther King"/>
|
||||
<additional_answer answer="Martin Luther King Junior"/>
|
||||
<textline size="20"/>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Test Explanation.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</stringresponse>
|
||||
</problem>`);
|
||||
});
|
||||
it('converts StringResponse with multiple answers and regular expressions to xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`Write a number from 1 to 4.
|
||||
=| ^One$
|
||||
or= two
|
||||
or= ^thre+
|
||||
or= ^4|Four$
|
||||
|
||||
[Explanation]
|
||||
Test Explanation.
|
||||
[Explanation]\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<stringresponse answer="^One$" type="ci regexp">
|
||||
<p>Write a number from 1 to 4.</p>
|
||||
<additional_answer answer="two"/>
|
||||
<additional_answer answer="^thre+"/>
|
||||
<additional_answer answer="^4|Four$"/>
|
||||
<textline size="20"/>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Test Explanation.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</stringresponse>
|
||||
</problem>`);
|
||||
});
|
||||
// test labels
|
||||
it('converts markdown labels to label attributes', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`>>Who lead the civil right movement in the United States of America?<<
|
||||
= | \w*\.?\s*Luther King\s*.*
|
||||
|
||||
[Explanation]
|
||||
Test Explanation.
|
||||
[Explanation]\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<stringresponse answer="w*.?s*Luther Kings*.*" type="ci regexp">
|
||||
<label>Who lead the civil right movement in the United States of America?</label>
|
||||
<textline size="20"/>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Test Explanation.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</stringresponse>
|
||||
</problem>`);
|
||||
});
|
||||
it('handles multiple questions with labels', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
France is a country in Europe.
|
||||
|
||||
>>What is the capital of France?<<
|
||||
= Paris
|
||||
|
||||
Germany is a country in Europe, too.
|
||||
|
||||
>>What is the capital of Germany?<<
|
||||
( ) Bonn
|
||||
( ) Hamburg
|
||||
(x) Berlin
|
||||
( ) Donut\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<p>France is a country in Europe.</p>
|
||||
|
||||
<label>What is the capital of France?</label>
|
||||
<stringresponse answer="Paris" type="ci" >
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
<p>Germany is a country in Europe, too.</p>
|
||||
|
||||
<label>What is the capital of Germany?</label>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Bonn</choice>
|
||||
<choice correct="false">Hamburg</choice>
|
||||
<choice correct="true">Berlin</choice>
|
||||
<choice correct="false">Donut</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
</problem>`);
|
||||
});
|
||||
it('tests multiple questions with only one label', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
France is a country in Europe.
|
||||
|
||||
>>What is the capital of France?<<
|
||||
= Paris
|
||||
|
||||
Germany is a country in Europe, too.
|
||||
|
||||
What is the capital of Germany?
|
||||
( ) Bonn
|
||||
( ) Hamburg
|
||||
(x) Berlin
|
||||
( ) Donut\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<p>France is a country in Europe.</p>
|
||||
|
||||
<label>What is the capital of France?</label>
|
||||
<stringresponse answer="Paris" type="ci" >
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
<p>Germany is a country in Europe, too.</p>
|
||||
|
||||
<p>What is the capital of Germany?</p>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Bonn</choice>
|
||||
<choice correct="false">Hamburg</choice>
|
||||
<choice correct="true">Berlin</choice>
|
||||
<choice correct="false">Donut</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
</problem>`);
|
||||
});
|
||||
|
||||
it('adds labels to formulae', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>Enter the numerical value of Pi:<<
|
||||
= 3.14159 +- .02\
|
||||
`);
|
||||
expect(data).toXMLEqual(`<problem>
|
||||
<numericalresponse answer="3.14159">
|
||||
<label>Enter the numerical value of Pi:</label>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
|
||||
|
||||
</problem>`);
|
||||
});
|
||||
|
||||
// test oddities
|
||||
it('converts headers and oddities to xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`Not a header
|
||||
A header
|
||||
==============
|
||||
|
||||
Multiple choice w/ parentheticals
|
||||
( ) option (with parens)
|
||||
( ) xd option (x)
|
||||
()) parentheses inside
|
||||
() no space b4 close paren
|
||||
|
||||
Choice checks
|
||||
[ ] option1 [x]
|
||||
[x] correct
|
||||
[x] redundant
|
||||
[(] distractor
|
||||
[] no space
|
||||
|
||||
Option with multiple correct ones
|
||||
[[one option, (correct one), (should not be correct)]]
|
||||
|
||||
Option with embedded parens
|
||||
[[My (heart), another, (correct)]]
|
||||
|
||||
What happens w/ empty correct options?
|
||||
[[()]]
|
||||
|
||||
[Explanation]see[/expLanation]
|
||||
|
||||
[explanation]
|
||||
orphaned start
|
||||
|
||||
No p tags in the below
|
||||
<script type='javascript'>
|
||||
var two = 2;
|
||||
|
||||
console.log(two * 2);
|
||||
</script>
|
||||
|
||||
But in this there should be
|
||||
<div>
|
||||
Great ideas require offsetting.
|
||||
|
||||
bad tests require drivel
|
||||
</div>
|
||||
|
||||
[code]
|
||||
Code should be nicely monospaced.
|
||||
[/code]\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<p>Not a header</p>
|
||||
<h3 class="hd hd-2 problem-header">A header</h3>
|
||||
<p>Multiple choice w/ parentheticals</p>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">option (with parens)</choice>
|
||||
<choice correct="false">xd option (x)</choice>
|
||||
<choice correct="false">parentheses inside</choice>
|
||||
<choice correct="false">no space b4 close paren</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
<p>Choice checks</p>
|
||||
<choiceresponse>
|
||||
<checkboxgroup>
|
||||
<choice correct="false">option1 [x]</choice>
|
||||
<choice correct="true">correct</choice>
|
||||
<choice correct="true">redundant</choice>
|
||||
<choice correct="false">distractor</choice>
|
||||
<choice correct="false">no space</choice>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
<p>Option with multiple correct ones</p>
|
||||
<optionresponse>
|
||||
<optioninput options="('one option','correct one','should not be correct')" correct="correct one"></optioninput>
|
||||
</optionresponse>
|
||||
<p>Option with embedded parens</p>
|
||||
<optionresponse>
|
||||
<optioninput options="('My (heart)','another','correct')" correct="correct"></optioninput>
|
||||
</optionresponse>
|
||||
<p>What happens w/ empty correct options?</p>
|
||||
<optionresponse>
|
||||
<optioninput options="('')" correct=""></optioninput>
|
||||
</optionresponse>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>see</p>
|
||||
</div>
|
||||
</solution>
|
||||
<p>[explanation]</p>
|
||||
<p>orphaned start</p>
|
||||
<p>No p tags in the below</p>
|
||||
<script type='javascript'>
|
||||
var two = 2;
|
||||
|
||||
console.log(two * 2);
|
||||
</script>
|
||||
<p>But in this there should be</p>
|
||||
<div>
|
||||
<p>Great ideas require offsetting.</p>
|
||||
<p>bad tests require drivel</p>
|
||||
</div>
|
||||
<pre>
|
||||
<code>Code should be nicely monospaced.
|
||||
</code>
|
||||
</pre>
|
||||
</problem>`);
|
||||
});
|
||||
|
||||
it('can separate responsetypes based on ---', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
Multiple choice problems allow learners to select only one option. Learners can see all the options along with the problem text.
|
||||
|
||||
>>Which of the following countries has the largest population?<<
|
||||
( ) Brazil {{ timely feedback -- explain why an almost correct answer is wrong }}
|
||||
( ) Germany
|
||||
(x) Indonesia
|
||||
( ) Russia
|
||||
|
||||
[explanation]
|
||||
According to September 2014 estimates:
|
||||
The population of Indonesia is approximately 250 million.
|
||||
The population of Brazil is approximately 200 million.
|
||||
The population of Russia is approximately 146 million.
|
||||
The population of Germany is approximately 81 million.
|
||||
[explanation]
|
||||
|
||||
---
|
||||
|
||||
Checkbox problems allow learners to select multiple options. Learners can see all the options along with the problem text.
|
||||
|
||||
>>The following languages are in the Indo-European family:<<
|
||||
[x] Urdu
|
||||
[ ] Finnish
|
||||
[x] Marathi
|
||||
[x] French
|
||||
[ ] Hungarian
|
||||
|
||||
Note: Make sure you select all of the correct options—there may be more than one!
|
||||
|
||||
[explanation]
|
||||
Urdu, Marathi, and French are all Indo-European languages, while Finnish and Hungarian are in the Uralic family.
|
||||
[explanation]
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<multiplechoiceresponse>
|
||||
<p>Multiple choice problems allow learners to select only one option. Learners can see all the options along with the problem text.</p>
|
||||
<label>Which of the following countries has the largest population?</label>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Brazil <choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Germany</choice>
|
||||
<choice correct="true">Indonesia</choice>
|
||||
<choice correct="false">Russia</choice>
|
||||
</choicegroup>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>According to September 2014 estimates:</p>
|
||||
<p>The population of Indonesia is approximately 250 million.</p>
|
||||
<p>The population of Brazil is approximately 200 million.</p>
|
||||
<p>The population of Russia is approximately 146 million.</p>
|
||||
<p>The population of Germany is approximately 81 million.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<choiceresponse>
|
||||
<p>Checkbox problems allow learners to select multiple options. Learners can see all the options along with the problem text.</p>
|
||||
<label>The following languages are in the Indo-European family:</label>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">Urdu</choice>
|
||||
<choice correct="false">Finnish</choice>
|
||||
<choice correct="true">Marathi</choice>
|
||||
<choice correct="true">French</choice>
|
||||
<choice correct="false">Hungarian</choice>
|
||||
</checkboxgroup>
|
||||
<p>Note: Make sure you select all of the correct options—there may be more than one!</p>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Urdu, Marathi, and French are all Indo-European languages, while Finnish and Hungarian are in the Uralic family.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</choiceresponse>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('can separate other things based on ---', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
Multiple choice problems allow learners to select only one option. Learners can see all the options along with the problem text.
|
||||
|
||||
---
|
||||
|
||||
>>Which of the following countries has the largest population?<<
|
||||
( ) Brazil {{ timely feedback -- explain why an almost correct answer is wrong }}
|
||||
( ) Germany
|
||||
(x) Indonesia
|
||||
( ) Russia
|
||||
|
||||
[explanation]
|
||||
According to September 2014 estimates:
|
||||
The population of Indonesia is approximately 250 million.
|
||||
The population of Brazil is approximately 200 million.
|
||||
The population of Russia is approximately 146 million.
|
||||
The population of Germany is approximately 81 million.
|
||||
[explanation]\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<p>Multiple choice problems allow learners to select only one option. Learners can see all the options along with the problem text.</p>
|
||||
|
||||
<multiplechoiceresponse>
|
||||
<label>Which of the following countries has the largest population?</label>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Brazil <choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Germany</choice>
|
||||
<choice correct="true">Indonesia</choice>
|
||||
<choice correct="false">Russia</choice>
|
||||
</choicegroup>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>According to September 2014 estimates:</p>
|
||||
<p>The population of Indonesia is approximately 250 million.</p>
|
||||
<p>The population of Brazil is approximately 200 million.</p>
|
||||
<p>The population of Russia is approximately 146 million.</p>
|
||||
<p>The population of Germany is approximately 81 million.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</multiplechoiceresponse>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('can do separation if spaces are present around ---', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>The following languages are in the Indo-European family:||There are three correct choices.<<
|
||||
[x] Urdu
|
||||
[ ] Finnish
|
||||
[x] Marathi
|
||||
[x] French
|
||||
[ ] Hungarian
|
||||
|
||||
---
|
||||
|
||||
>>Which of the following countries has the largest population?||You have only choice.<<
|
||||
( ) Brazil {{ timely feedback -- explain why an almost correct answer is wrong }}
|
||||
( ) Germany
|
||||
(x) Indonesia
|
||||
( ) Russia\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<choiceresponse>
|
||||
<label>The following languages are in the Indo-European family:</label>
|
||||
<description>There are three correct choices.</description>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">Urdu</choice>
|
||||
<choice correct="false">Finnish</choice>
|
||||
<choice correct="true">Marathi</choice>
|
||||
<choice correct="true">French</choice>
|
||||
<choice correct="false">Hungarian</choice>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
|
||||
<multiplechoiceresponse>
|
||||
<label>Which of the following countries has the largest population?</label>
|
||||
<description>You have only choice.</description>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Brazil
|
||||
<choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Germany</choice>
|
||||
<choice correct="true">Indonesia</choice>
|
||||
<choice correct="false">Russia</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('can extract question description', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>The following languages are in the Indo-European family:||Choose wisely.<<
|
||||
[x] Urdu
|
||||
[ ] Finnish
|
||||
[x] Marathi
|
||||
[x] French
|
||||
[ ] Hungarian\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<choiceresponse>
|
||||
<label>The following languages are in the Indo-European family:</label>
|
||||
<description>Choose wisely.</description>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">Urdu</choice>
|
||||
<choice correct="false">Finnish</choice>
|
||||
<choice correct="true">Marathi</choice>
|
||||
<choice correct="true">French</choice>
|
||||
<choice correct="false">Hungarian</choice>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('can handle question and description spanned across multiple lines', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>The following languages
|
||||
are in the
|
||||
Indo-European family:
|
||||
||
|
||||
first second
|
||||
third
|
||||
<<
|
||||
[x] Urdu
|
||||
[ ] Finnish
|
||||
[x] Marathi\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<choiceresponse>
|
||||
<label>The following languages are in the Indo-European family:</label>
|
||||
<description>first second third</description>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">Urdu</choice>
|
||||
<choice correct="false">Finnish</choice>
|
||||
<choice correct="true">Marathi</choice>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('will not add empty description', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>The following languages are in the Indo-European family:||<<
|
||||
[x] Urdu
|
||||
[ ] Finnish\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<choiceresponse>
|
||||
<label>The following languages are in the Indo-European family:</label>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">Urdu</choice>
|
||||
<choice correct="false">Finnish</choice>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,996 +0,0 @@
|
||||
# This file tests the parsing of extended-hints, double bracket sections {{ .. }}
|
||||
# for all sorts of markdown.
|
||||
describe 'Markdown to xml extended hint dropdown', ->
|
||||
it 'produces xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
Translation between Dropdown and ________ is straightforward.
|
||||
|
||||
[[
|
||||
(Multiple Choice) {{ Good Job::Yes, multiple choice is the right answer. }}
|
||||
Text Input {{ No, text input problems don't present options. }}
|
||||
Numerical Input {{ No, numerical input problems don't present options. }}
|
||||
]]
|
||||
|
||||
|
||||
|
||||
Clowns have funny _________ to make people laugh.
|
||||
|
||||
[[
|
||||
dogs {{ NOPE::Not dogs, not cats, not toads }}
|
||||
(FACES) {{ With lots of makeup, doncha know?}}
|
||||
|
||||
money {{ Clowns don't have any money, of course }}
|
||||
donkeys {{don't be an ass.}}
|
||||
-no hint-
|
||||
]]
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<p>Translation between Dropdown and ________ is straightforward.</p>
|
||||
<optionresponse>
|
||||
<optioninput>
|
||||
<option correct="True">Multiple Choice
|
||||
<optionhint label="Good Job">Yes, multiple choice is the right answer.</optionhint>
|
||||
</option>
|
||||
<option correct="False">Text Input
|
||||
<optionhint>No, text input problems don't present options.</optionhint>
|
||||
</option>
|
||||
<option correct="False">Numerical Input
|
||||
<optionhint>No, numerical input problems don't present options.</optionhint>
|
||||
</option>
|
||||
</optioninput>
|
||||
</optionresponse>
|
||||
<p>Clowns have funny _________ to make people laugh.</p>
|
||||
<optionresponse>
|
||||
<optioninput>
|
||||
<option correct="False">dogs
|
||||
<optionhint label="NOPE">Not dogs, not cats, not toads</optionhint>
|
||||
</option>
|
||||
<option correct="True">FACES
|
||||
<optionhint>With lots of makeup, doncha know?</optionhint>
|
||||
</option>
|
||||
<option correct="False">money
|
||||
<optionhint>Clowns don't have any money, of course</optionhint>
|
||||
</option>
|
||||
<option correct="False">donkeys
|
||||
<optionhint>don't be an ass.</optionhint>
|
||||
</option>
|
||||
<option correct="False">-no hint-</option>
|
||||
</optioninput>
|
||||
</optionresponse>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with demand hint', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
Translation between Dropdown and ________ is straightforward.
|
||||
|
||||
[[
|
||||
(Right) {{ Good Job::yes }}
|
||||
Wrong 1 {{no}}
|
||||
Wrong 2 {{ Label::no }}
|
||||
]]
|
||||
|
||||
|| 0) zero ||
|
||||
|| 1) one ||
|
||||
|| 2) two ||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<optionresponse>
|
||||
<p>Translation between Dropdown and ________ is straightforward.</p>
|
||||
<optioninput>
|
||||
<option correct="True">Right <optionhint label="Good Job">yes</optionhint>
|
||||
</option>
|
||||
<option correct="False">Wrong 1 <optionhint>no</optionhint>
|
||||
</option>
|
||||
<option correct="False">Wrong 2 <optionhint label="Label">no</optionhint>
|
||||
</option>
|
||||
</optioninput>
|
||||
</optionresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>0) zero</hint>
|
||||
<hint>1) one</hint>
|
||||
<hint>2) two</hint>
|
||||
</demandhint>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with single-line markdown syntax', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
A Question ________ is answered.
|
||||
|
||||
[[(Right), Wrong 1, Wrong 2]]
|
||||
|| 0) zero ||
|
||||
|| 1) one ||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<optionresponse>
|
||||
<p>A Question ________ is answered.</p>
|
||||
<optioninput options="('Right','Wrong 1','Wrong 2')" correct="Right"/>
|
||||
</optionresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>0) zero</hint>
|
||||
<hint>1) one</hint>
|
||||
</demandhint>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with fewer newlines', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>q1<<
|
||||
[[ (aa) {{ hint1 }}
|
||||
bb
|
||||
cc {{ hint2 }} ]]
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<optionresponse>
|
||||
<label>q1</label>
|
||||
<optioninput>
|
||||
<option correct="True">aa <optionhint>hint1</optionhint>
|
||||
</option>
|
||||
<option correct="False">bb</option>
|
||||
<option correct="False">cc <optionhint>hint2</optionhint>
|
||||
</option>
|
||||
</optioninput>
|
||||
</optionresponse>
|
||||
|
||||
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml even with lots of whitespace', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>q1<<
|
||||
[[
|
||||
|
||||
|
||||
aa {{ hint1 }}
|
||||
|
||||
bb {{ hint2 }}
|
||||
(cc)
|
||||
|
||||
]]
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<optionresponse>
|
||||
<label>q1</label>
|
||||
<optioninput>
|
||||
<option correct="False">aa <optionhint>hint1</optionhint>
|
||||
</option>
|
||||
<option correct="False">bb <optionhint>hint2</optionhint>
|
||||
</option>
|
||||
<option correct="True">cc</option>
|
||||
</optioninput>
|
||||
</optionresponse>
|
||||
|
||||
|
||||
</problem>
|
||||
""")
|
||||
|
||||
describe 'Markdown to xml extended hint checkbox', ->
|
||||
it 'produces xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>Select all the fruits from the list<<
|
||||
|
||||
[x] Apple {{ selected: You're right that apple is a fruit. }, {unselected: Remember that apple is also a fruit.}}
|
||||
[ ] Mushroom {{U: You're right that mushrooms aren't fruit}, { selected: Mushroom is a fungus, not a fruit.}}
|
||||
[x] Grape {{ selected: You're right that grape is a fruit }, {unselected: Remember that grape is also a fruit.}}
|
||||
[ ] Mustang
|
||||
[ ] Camero {{S:I don't know what a Camero is but it isn't a fruit.},{U:What is a camero anyway?}}
|
||||
|
||||
|
||||
{{ ((A*B)) You're right that apple is a fruit, but there's one you're missing. Also, mushroom is not a fruit.}}
|
||||
{{ ((B*C)) You're right that grape is a fruit, but there's one you're missing. Also, mushroom is not a fruit. }}
|
||||
|
||||
|
||||
>>Select all the vegetables from the list<<
|
||||
|
||||
[ ] Banana {{ selected: No, sorry, a banana is a fruit. }, {unselected: poor banana.}}
|
||||
[ ] Ice Cream
|
||||
[ ] Mushroom {{U: You're right that mushrooms aren't vegetables.}, { selected: Mushroom is a fungus, not a vegetable.}}
|
||||
[x] Brussel Sprout {{S: Brussel sprouts are vegetables.}, {u: Brussel sprout is the only vegetable in this list.}}
|
||||
|
||||
|
||||
{{ ((A*B)) Making a banana split? }}
|
||||
{{ ((B*D)) That will make a horrible dessert: a brussel sprout split? }}
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<label>Select all the fruits from the list</label>
|
||||
<choiceresponse>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">Apple
|
||||
<choicehint selected="true">You're right that apple is a fruit.</choicehint>
|
||||
<choicehint selected="false">Remember that apple is also a fruit.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint selected="true">Mushroom is a fungus, not a fruit.</choicehint>
|
||||
<choicehint selected="false">You're right that mushrooms aren't fruit</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">Grape
|
||||
<choicehint selected="true">You're right that grape is a fruit</choicehint>
|
||||
<choicehint selected="false">Remember that grape is also a fruit.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Mustang</choice>
|
||||
<choice correct="false">Camero
|
||||
<choicehint selected="true">I don't know what a Camero is but it isn't a fruit.</choicehint>
|
||||
<choicehint selected="false">What is a camero anyway?</choicehint>
|
||||
</choice>
|
||||
<compoundhint value="A*B">You're right that apple is a fruit, but there's one you're missing. Also, mushroom is not a fruit.</compoundhint>
|
||||
<compoundhint value="B*C">You're right that grape is a fruit, but there's one you're missing. Also, mushroom is not a fruit.</compoundhint>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
|
||||
<label>Select all the vegetables from the list</label>
|
||||
<choiceresponse>
|
||||
<checkboxgroup>
|
||||
<choice correct="false">Banana
|
||||
<choicehint selected="true">No, sorry, a banana is a fruit.</choicehint>
|
||||
<choicehint selected="false">poor banana.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Ice Cream</choice>
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint selected="true">Mushroom is a fungus, not a vegetable.</choicehint>
|
||||
<choicehint selected="false">You're right that mushrooms aren't vegetables.</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">Brussel Sprout
|
||||
<choicehint selected="true">Brussel sprouts are vegetables.</choicehint>
|
||||
<choicehint selected="false">Brussel sprout is the only vegetable in this list.</choicehint>
|
||||
</choice>
|
||||
<compoundhint value="A*B">Making a banana split?</compoundhint>
|
||||
<compoundhint value="B*D">That will make a horrible dessert: a brussel sprout split?</compoundhint>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml also with demand hints', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>Select all the fruits from the list<<
|
||||
|
||||
[x] Apple {{ selected: You're right that apple is a fruit. }, {unselected: Remember that apple is also a fruit.}}
|
||||
[ ] Mushroom {{U: You're right that mushrooms aren't fruit}, { selected: Mushroom is a fungus, not a fruit.}}
|
||||
[x] Grape {{ selected: You're right that grape is a fruit }, {unselected: Remember that grape is also a fruit.}}
|
||||
[ ] Mustang
|
||||
[ ] Camero {{S:I don't know what a Camero is but it isn't a fruit.},{U:What is a camero anyway?}}
|
||||
|
||||
{{ ((A*B)) You're right that apple is a fruit, but there's one you're missing. Also, mushroom is not a fruit.}}
|
||||
{{ ((B*C)) You're right that grape is a fruit, but there's one you're missing. Also, mushroom is not a fruit.}}
|
||||
|
||||
>>Select all the vegetables from the list<<
|
||||
|
||||
[ ] Banana {{ selected: No, sorry, a banana is a fruit. }, {unselected: poor banana.}}
|
||||
[ ] Ice Cream
|
||||
[ ] Mushroom {{U: You're right that mushrooms aren't vegatbles}, { selected: Mushroom is a fungus, not a vegetable.}}
|
||||
[x] Brussel Sprout {{S: Brussel sprouts are vegetables.}, {u: Brussel sprout is the only vegetable in this list.}}
|
||||
|
||||
{{ ((A*B)) Making a banana split? }}
|
||||
{{ ((B*D)) That will make a horrible dessert: a brussel sprout split? }}
|
||||
|
||||
|| Hint one.||
|
||||
|| Hint two. ||
|
||||
|| Hint three. ||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<label>Select all the fruits from the list</label>
|
||||
<choiceresponse>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">Apple
|
||||
<choicehint selected="true">You're right that apple is a fruit.</choicehint>
|
||||
<choicehint selected="false">Remember that apple is also a fruit.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint selected="true">Mushroom is a fungus, not a fruit.</choicehint>
|
||||
<choicehint selected="false">You're right that mushrooms aren't fruit</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">Grape
|
||||
<choicehint selected="true">You're right that grape is a fruit</choicehint>
|
||||
<choicehint selected="false">Remember that grape is also a fruit.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Mustang</choice>
|
||||
<choice correct="false">Camero
|
||||
<choicehint selected="true">I don't know what a Camero is but it isn't a fruit.</choicehint>
|
||||
<choicehint selected="false">What is a camero anyway?</choicehint>
|
||||
</choice>
|
||||
<compoundhint value="A*B">You're right that apple is a fruit, but there's one you're missing. Also, mushroom is not a fruit.</compoundhint>
|
||||
<compoundhint value="B*C">You're right that grape is a fruit, but there's one you're missing. Also, mushroom is not a fruit.</compoundhint>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
|
||||
<label>Select all the vegetables from the list</label>
|
||||
<choiceresponse>
|
||||
<checkboxgroup>
|
||||
<choice correct="false">Banana
|
||||
<choicehint selected="true">No, sorry, a banana is a fruit.</choicehint>
|
||||
<choicehint selected="false">poor banana.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Ice Cream</choice>
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint selected="true">Mushroom is a fungus, not a vegetable.</choicehint>
|
||||
<choicehint selected="false">You're right that mushrooms aren't vegatbles</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">Brussel Sprout
|
||||
<choicehint selected="true">Brussel sprouts are vegetables.</choicehint>
|
||||
<choicehint selected="false">Brussel sprout is the only vegetable in this list.</choicehint>
|
||||
</choice>
|
||||
<compoundhint value="A*B">Making a banana split?</compoundhint>
|
||||
<compoundhint value="B*D">That will make a horrible dessert: a brussel sprout split?</compoundhint>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>Hint one.</hint>
|
||||
<hint>Hint two.</hint>
|
||||
<hint>Hint three.</hint>
|
||||
</demandhint>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
|
||||
describe 'Markdown to xml extended hint multiple choice', ->
|
||||
it 'produces xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>Select the fruit from the list<<
|
||||
|
||||
() Mushroom {{ Mushroom is a fungus, not a fruit.}}
|
||||
() Potato
|
||||
(x) Apple {{ OUTSTANDING::Apple is indeed a fruit.}}
|
||||
|
||||
>>Select the vegetables from the list<<
|
||||
|
||||
() Mushroom {{ Mushroom is a fungus, not a vegetable.}}
|
||||
(x) Potato {{ Potato is a root vegetable. }}
|
||||
() Apple {{ OOPS::Apple is a fruit.}}
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<label>Select the fruit from the list</label>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint>Mushroom is a fungus, not a fruit.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Potato</choice>
|
||||
<choice correct="true">Apple
|
||||
<choicehint label="OUTSTANDING">Apple is indeed a fruit.</choicehint>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<label>Select the vegetables from the list</label>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint>Mushroom is a fungus, not a vegetable.</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">Potato
|
||||
<choicehint>Potato is a root vegetable.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Apple
|
||||
<choicehint label="OOPS">Apple is a fruit.</choicehint>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with demand hints', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>Select the fruit from the list<<
|
||||
|
||||
() Mushroom {{ Mushroom is a fungus, not a fruit.}}
|
||||
() Potato
|
||||
(x) Apple {{ OUTSTANDING::Apple is indeed a fruit.}}
|
||||
|
||||
|| 0) spaces on previous line. ||
|
||||
|| 1) roses are red. ||
|
||||
|
||||
>>Select the vegetables from the list<<
|
||||
|
||||
() Mushroom {{ Mushroom is a fungus, not a vegetable.}}
|
||||
(x) Potato {{ Potato is a root vegetable. }}
|
||||
() Apple {{ OOPS::Apple is a fruit.}}
|
||||
|
||||
|| 2) where are the lions? ||
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<label>Select the fruit from the list</label>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint>Mushroom is a fungus, not a fruit.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Potato</choice>
|
||||
<choice correct="true">Apple
|
||||
<choicehint label="OUTSTANDING">Apple is indeed a fruit.</choicehint>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<label>Select the vegetables from the list</label>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint>Mushroom is a fungus, not a vegetable.</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">Potato
|
||||
<choicehint>Potato is a root vegetable.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Apple
|
||||
<choicehint label="OOPS">Apple is a fruit.</choicehint>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>0) spaces on previous line.</hint>
|
||||
<hint>1) roses are red.</hint>
|
||||
<hint>2) where are the lions?</hint>
|
||||
</demandhint>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
|
||||
describe 'Markdown to xml extended hint text input', ->
|
||||
it 'produces xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml(""">>In which country would you find the city of Paris?<<
|
||||
= France {{ BRAVO::Viva la France! }}
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<stringresponse answer="France" type="ci">
|
||||
<label>In which country would you find the city of Paris?</label>
|
||||
<correcthint label="BRAVO">Viva la France!</correcthint>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with or=', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml(""">>Where Paris?<<
|
||||
= France {{ BRAVO::hint1}}
|
||||
or= USA {{ meh::hint2 }}
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<stringresponse answer="France" type="ci">
|
||||
<label>Where Paris?</label>
|
||||
<correcthint label="BRAVO">hint1</correcthint>
|
||||
<additional_answer answer="USA"><correcthint label="meh">hint2</correcthint>
|
||||
</additional_answer>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with not=', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml(""">>Revenge is a dish best served<<
|
||||
= cold {{khaaaaaan!}}
|
||||
not= warm {{feedback2}}
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<stringresponse answer="cold" type="ci">
|
||||
<label>Revenge is a dish best served</label>
|
||||
<correcthint>khaaaaaan!</correcthint>
|
||||
<stringequalhint answer="warm">feedback2</stringequalhint>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with s=', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml(""">>q<<
|
||||
s= 2 {{feedback1}}
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<stringresponse answer="2" type="ci">
|
||||
<label>q</label>
|
||||
<correcthint>feedback1</correcthint>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with = and or= and not=', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml(""">>q<<
|
||||
= aaa
|
||||
or= bbb {{feedback1}}
|
||||
not= no {{feedback2}}
|
||||
or= ccc
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<stringresponse answer="aaa" type="ci">
|
||||
<label>q</label>
|
||||
<additional_answer answer="bbb"><correcthint>feedback1</correcthint>
|
||||
</additional_answer>
|
||||
<stringequalhint answer="no">feedback2</stringequalhint>
|
||||
<additional_answer answer="ccc"/>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with s= and or=', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml(""">>q<<
|
||||
s= 2 {{feedback1}}
|
||||
or= bbb {{feedback2}}
|
||||
or= ccc
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<stringresponse answer="2" type="ci">
|
||||
<label>q</label>
|
||||
<correcthint>feedback1</correcthint>
|
||||
<additional_answer answer="bbb"><correcthint>feedback2</correcthint>
|
||||
</additional_answer>
|
||||
<additional_answer answer="ccc"/>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with each = making a new question', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>q<<
|
||||
= aaa
|
||||
or= bbb
|
||||
s= ccc
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<label>q</label>
|
||||
<stringresponse answer="aaa" type="ci">
|
||||
<additional_answer answer="bbb"></additional_answer>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
<stringresponse answer="ccc" type="ci">
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with each = making a new question amid blank lines and paragraphs', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
paragraph
|
||||
>>q<<
|
||||
= aaa
|
||||
|
||||
or= bbb
|
||||
s= ccc
|
||||
|
||||
paragraph 2
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<p>paragraph</p>
|
||||
<label>q</label>
|
||||
<stringresponse answer="aaa" type="ci">
|
||||
<additional_answer answer="bbb"></additional_answer>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
<stringresponse answer="ccc" type="ci">
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
<p>paragraph 2</p>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml without a question when or= is just hung out there by itself', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
paragraph
|
||||
>>q<<
|
||||
or= aaa
|
||||
paragraph 2
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<p>paragraph</p>
|
||||
<label>q</label>
|
||||
<p>or= aaa</p>
|
||||
<p>paragraph 2</p>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with each = with feedback making a new question', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>q<<
|
||||
s= aaa
|
||||
or= bbb {{feedback1}}
|
||||
= ccc {{feedback2}}
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<label>q</label>
|
||||
<stringresponse answer="aaa" type="ci">
|
||||
<additional_answer answer="bbb">
|
||||
<correcthint>feedback1</correcthint>
|
||||
</additional_answer>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
<stringresponse answer="ccc" type="ci">
|
||||
<correcthint>feedback2</correcthint>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with demand hints', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml(""">>Where Paris?<<
|
||||
= France {{ BRAVO::hint1 }}
|
||||
|
||||
|| There are actually two countries with cities named Paris. ||
|
||||
|| Paris is the capital of one of those countries. ||
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<stringresponse answer="France" type="ci">
|
||||
<label>Where Paris?</label>
|
||||
<correcthint label="BRAVO">hint1</correcthint>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>There are actually two countries with cities named Paris.</hint>
|
||||
<hint>Paris is the capital of one of those countries.</hint>
|
||||
</demandhint>
|
||||
</problem>""")
|
||||
|
||||
|
||||
describe 'Markdown to xml extended hint numeric input', ->
|
||||
it 'produces xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>Enter the numerical value of Pi:<<
|
||||
= 3.14159 +- .02 {{ Pie for everyone! }}
|
||||
|
||||
>>Enter the approximate value of 502*9:<<
|
||||
= 4518 +- 15% {{PIE:: No pie for you!}}
|
||||
|
||||
>>Enter the number of fingers on a human hand<<
|
||||
= 5
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<label>Enter the numerical value of Pi:</label>
|
||||
<numericalresponse answer="3.14159">
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<formulaequationinput/>
|
||||
<correcthint>Pie for everyone!</correcthint>
|
||||
</numericalresponse>
|
||||
|
||||
<label>Enter the approximate value of 502*9:</label>
|
||||
<numericalresponse answer="4518">
|
||||
<responseparam type="tolerance" default="15%"/>
|
||||
<formulaequationinput/>
|
||||
<correcthint label="PIE">No pie for you!</correcthint>
|
||||
</numericalresponse>
|
||||
|
||||
<label>Enter the number of fingers on a human hand</label>
|
||||
<numericalresponse answer="5">
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
# The output xml here shows some of the quirks of how historical markdown parsing does or does not put
|
||||
# in blank lines.
|
||||
it 'numeric input with hints and demand hints', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>text1<<
|
||||
= 1 {{ hint1 }}
|
||||
|| hintA ||
|
||||
>>text2<<
|
||||
= 2 {{ hint2 }}
|
||||
|
||||
|| hintB ||
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<label>text1</label>
|
||||
<numericalresponse answer="1">
|
||||
<formulaequationinput/>
|
||||
<correcthint>hint1</correcthint>
|
||||
</numericalresponse>
|
||||
<label>text2</label>
|
||||
<numericalresponse answer="2">
|
||||
<formulaequationinput/>
|
||||
<correcthint>hint2</correcthint>
|
||||
</numericalresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>hintA</hint>
|
||||
<hint>hintB</hint>
|
||||
</demandhint>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
|
||||
describe 'Markdown to xml extended hint with multiline hints', ->
|
||||
it 'produces xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>Checkboxes<<
|
||||
|
||||
[x] A {{
|
||||
selected: aaa },
|
||||
{unselected:bbb}}
|
||||
[ ] B {{U: c}, {
|
||||
selected: d.}}
|
||||
|
||||
{{ ((A*B)) A*B hint}}
|
||||
|
||||
>>What is 1 + 1?<<
|
||||
= 2 {{ part one, and
|
||||
part two
|
||||
}}
|
||||
|
||||
>>hello?<<
|
||||
= hello {{
|
||||
hello
|
||||
hint
|
||||
}}
|
||||
|
||||
>>multiple choice<<
|
||||
(x) AA{{hint1}}
|
||||
() BB {{
|
||||
hint2
|
||||
}}
|
||||
( ) CC {{ hint3
|
||||
}}
|
||||
|
||||
>>dropdown<<
|
||||
[[
|
||||
W1 {{
|
||||
no }}
|
||||
W2 {{
|
||||
nope}}
|
||||
(C1) {{ yes
|
||||
}}
|
||||
]]
|
||||
|
||||
|| aaa ||
|
||||
||bbb||
|
||||
|| ccc ||
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<label>Checkboxes</label>
|
||||
<choiceresponse>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">A
|
||||
<choicehint selected="true">aaa</choicehint>
|
||||
<choicehint selected="false">bbb</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">B
|
||||
<choicehint selected="true">d.</choicehint>
|
||||
<choicehint selected="false">c</choicehint>
|
||||
</choice>
|
||||
<compoundhint value="A*B">A*B hint</compoundhint>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
|
||||
<label>What is 1 + 1?</label>
|
||||
<numericalresponse answer="2">
|
||||
<formulaequationinput/>
|
||||
<correcthint>part one, and part two</correcthint>
|
||||
</numericalresponse>
|
||||
|
||||
<label>hello?</label>
|
||||
<stringresponse answer="hello" type="ci">
|
||||
<correcthint>hello hint</correcthint>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
<label>multiple choice</label>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="true">AA
|
||||
<choicehint>hint1</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">BB
|
||||
<choicehint>hint2</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">CC
|
||||
<choicehint>hint3</choicehint>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<label>dropdown</label>
|
||||
<optionresponse>
|
||||
<optioninput>
|
||||
<option correct="False">W1
|
||||
<optionhint>no</optionhint>
|
||||
</option>
|
||||
<option correct="False">W2
|
||||
<optionhint>nope</optionhint>
|
||||
</option>
|
||||
<option correct="True">C1
|
||||
<optionhint>yes</optionhint>
|
||||
</option>
|
||||
</optioninput>
|
||||
</optionresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>aaa</hint>
|
||||
<hint>bbb</hint>
|
||||
<hint>ccc</hint>
|
||||
</demandhint>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
describe 'Markdown to xml extended hint with tricky syntax cases', ->
|
||||
# I'm entering this as utf-8 in this file.
|
||||
# I cannot find a way to set the encoding for .coffee files but it seems to work.
|
||||
it 'produces xml with unicode', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>á and Ø<<
|
||||
|
||||
(x) Ø{{Ø}}
|
||||
() BB
|
||||
|
||||
|| Ø ||
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<multiplechoiceresponse>
|
||||
<label>á and Ø</label>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="true">Ø
|
||||
<choicehint>Ø</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">BB</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>Ø</hint>
|
||||
</demandhint>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with quote-type characters', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>"quotes" aren't `fun`<<
|
||||
() "hello" {{ isn't }}
|
||||
(x) "isn't" {{ "hello" }}
|
||||
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<multiplechoiceresponse>
|
||||
<label>"quotes" aren't `fun`</label>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">"hello"
|
||||
<choicehint>isn't</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">"isn't"
|
||||
<choicehint>"hello"</choicehint>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
it 'produces xml with almost but not quite multiple choice syntax', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>q1<<
|
||||
this (x)
|
||||
() a {{ (hint) }}
|
||||
(x) b
|
||||
that (y)
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<multiplechoiceresponse>
|
||||
<label>q1</label>
|
||||
<p>this (x)</p>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">a <choicehint>(hint)</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">b</choice>
|
||||
</choicegroup>
|
||||
<p>that (y)</p>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
|
||||
</problem>
|
||||
""")
|
||||
|
||||
# An incomplete checkbox hint passes through to cue the author
|
||||
it 'produce xml with almost but not quite checkboxgroup syntax', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
>>q1<<
|
||||
this [x]
|
||||
[ ] a [square]
|
||||
[x] b {{ this hint passes through }}
|
||||
that []
|
||||
""")
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<choiceresponse>
|
||||
<label>q1</label>
|
||||
<p>this [x]</p>
|
||||
<checkboxgroup>
|
||||
<choice correct="false">a [square]</choice>
|
||||
<choice correct="true">b {{ this hint passes through }}</choice>
|
||||
</checkboxgroup>
|
||||
<p>that []</p>
|
||||
</choiceresponse>
|
||||
|
||||
|
||||
</problem>
|
||||
""")
|
||||
|
||||
# It's sort of a pain to edit DOS line endings without some editor or other "fixing" them
|
||||
# for you. Therefore, we construct DOS line endings on the fly just for the test.
|
||||
it 'produces xml with DOS \r\n line endings', ->
|
||||
markdown = """
|
||||
>>q22<<
|
||||
|
||||
[[
|
||||
(x) {{ hintx
|
||||
these
|
||||
span
|
||||
}}
|
||||
|
||||
yy {{ meh::hinty }}
|
||||
zzz {{ hintz }}
|
||||
]]
|
||||
"""
|
||||
markdown = markdown.replace(/\n/g, '\r\n') # make DOS line endings
|
||||
data = MarkdownEditingDescriptor.markdownToXml(markdown)
|
||||
expect(data).toXMLEqual("""
|
||||
<problem>
|
||||
<optionresponse>
|
||||
<label>q22</label>
|
||||
<optioninput>
|
||||
<option correct="True">x <optionhint>hintx these span</optionhint>
|
||||
</option>
|
||||
<option correct="False">yy <optionhint label="meh">hinty</optionhint>
|
||||
</option>
|
||||
<option correct="False">zzz <optionhint>hintz</optionhint>
|
||||
</option>
|
||||
</optioninput>
|
||||
</optionresponse>
|
||||
|
||||
|
||||
</problem>
|
||||
""")
|
||||
1031
common/lib/xmodule/xmodule/js/spec/problem/edit_spec_hint.js
Normal file
1031
common/lib/xmodule/xmodule/js/spec/problem/edit_spec_hint.js
Normal file
@@ -0,0 +1,1031 @@
|
||||
// This file tests the parsing of extended-hints, double bracket sections {{ .. }}
|
||||
// for all sorts of markdown.
|
||||
describe('Markdown to xml extended hint dropdown', function() {
|
||||
it('produces xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
Translation between Dropdown and ________ is straightforward.
|
||||
|
||||
[[
|
||||
(Multiple Choice) {{ Good Job::Yes, multiple choice is the right answer. }}
|
||||
Text Input {{ No, text input problems don't present options. }}
|
||||
Numerical Input {{ No, numerical input problems don't present options. }}
|
||||
]]
|
||||
|
||||
|
||||
|
||||
Clowns have funny _________ to make people laugh.
|
||||
|
||||
[[
|
||||
dogs {{ NOPE::Not dogs, not cats, not toads }}
|
||||
(FACES) {{ With lots of makeup, doncha know?}}
|
||||
|
||||
money {{ Clowns don't have any money, of course }}
|
||||
donkeys {{don't be an ass.}}
|
||||
-no hint-
|
||||
]]
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<p>Translation between Dropdown and ________ is straightforward.</p>
|
||||
<optionresponse>
|
||||
<optioninput>
|
||||
<option correct="True">Multiple Choice
|
||||
<optionhint label="Good Job">Yes, multiple choice is the right answer.</optionhint>
|
||||
</option>
|
||||
<option correct="False">Text Input
|
||||
<optionhint>No, text input problems don't present options.</optionhint>
|
||||
</option>
|
||||
<option correct="False">Numerical Input
|
||||
<optionhint>No, numerical input problems don't present options.</optionhint>
|
||||
</option>
|
||||
</optioninput>
|
||||
</optionresponse>
|
||||
<p>Clowns have funny _________ to make people laugh.</p>
|
||||
<optionresponse>
|
||||
<optioninput>
|
||||
<option correct="False">dogs
|
||||
<optionhint label="NOPE">Not dogs, not cats, not toads</optionhint>
|
||||
</option>
|
||||
<option correct="True">FACES
|
||||
<optionhint>With lots of makeup, doncha know?</optionhint>
|
||||
</option>
|
||||
<option correct="False">money
|
||||
<optionhint>Clowns don't have any money, of course</optionhint>
|
||||
</option>
|
||||
<option correct="False">donkeys
|
||||
<optionhint>don't be an ass.</optionhint>
|
||||
</option>
|
||||
<option correct="False">-no hint-</option>
|
||||
</optioninput>
|
||||
</optionresponse>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with demand hint', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
Translation between Dropdown and ________ is straightforward.
|
||||
|
||||
[[
|
||||
(Right) {{ Good Job::yes }}
|
||||
Wrong 1 {{no}}
|
||||
Wrong 2 {{ Label::no }}
|
||||
]]
|
||||
|
||||
|| 0) zero ||
|
||||
|| 1) one ||
|
||||
|| 2) two ||\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<optionresponse>
|
||||
<p>Translation between Dropdown and ________ is straightforward.</p>
|
||||
<optioninput>
|
||||
<option correct="True">Right <optionhint label="Good Job">yes</optionhint>
|
||||
</option>
|
||||
<option correct="False">Wrong 1 <optionhint>no</optionhint>
|
||||
</option>
|
||||
<option correct="False">Wrong 2 <optionhint label="Label">no</optionhint>
|
||||
</option>
|
||||
</optioninput>
|
||||
</optionresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>0) zero</hint>
|
||||
<hint>1) one</hint>
|
||||
<hint>2) two</hint>
|
||||
</demandhint>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with single-line markdown syntax', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
A Question ________ is answered.
|
||||
|
||||
[[(Right), Wrong 1, Wrong 2]]
|
||||
|| 0) zero ||
|
||||
|| 1) one ||\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<optionresponse>
|
||||
<p>A Question ________ is answered.</p>
|
||||
<optioninput options="('Right','Wrong 1','Wrong 2')" correct="Right"/>
|
||||
</optionresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>0) zero</hint>
|
||||
<hint>1) one</hint>
|
||||
</demandhint>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with fewer newlines', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>q1<<
|
||||
[[ (aa) {{ hint1 }}
|
||||
bb
|
||||
cc {{ hint2 }} ]]\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<optionresponse>
|
||||
<label>q1</label>
|
||||
<optioninput>
|
||||
<option correct="True">aa <optionhint>hint1</optionhint>
|
||||
</option>
|
||||
<option correct="False">bb</option>
|
||||
<option correct="False">cc <optionhint>hint2</optionhint>
|
||||
</option>
|
||||
</optioninput>
|
||||
</optionresponse>
|
||||
|
||||
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml even with lots of whitespace', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>q1<<
|
||||
[[
|
||||
|
||||
|
||||
aa {{ hint1 }}
|
||||
|
||||
bb {{ hint2 }}
|
||||
(cc)
|
||||
|
||||
]]\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<optionresponse>
|
||||
<label>q1</label>
|
||||
<optioninput>
|
||||
<option correct="False">aa <optionhint>hint1</optionhint>
|
||||
</option>
|
||||
<option correct="False">bb <optionhint>hint2</optionhint>
|
||||
</option>
|
||||
<option correct="True">cc</option>
|
||||
</optioninput>
|
||||
</optionresponse>
|
||||
|
||||
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Markdown to xml extended hint checkbox', function() {
|
||||
it('produces xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>Select all the fruits from the list<<
|
||||
|
||||
[x] Apple {{ selected: You're right that apple is a fruit. }, {unselected: Remember that apple is also a fruit.}}
|
||||
[ ] Mushroom {{U: You're right that mushrooms aren't fruit}, { selected: Mushroom is a fungus, not a fruit.}}
|
||||
[x] Grape {{ selected: You're right that grape is a fruit }, {unselected: Remember that grape is also a fruit.}}
|
||||
[ ] Mustang
|
||||
[ ] Camero {{S:I don't know what a Camero is but it isn't a fruit.},{U:What is a camero anyway?}}
|
||||
|
||||
|
||||
{{ ((A*B)) You're right that apple is a fruit, but there's one you're missing. Also, mushroom is not a fruit.}}
|
||||
{{ ((B*C)) You're right that grape is a fruit, but there's one you're missing. Also, mushroom is not a fruit. }}
|
||||
|
||||
|
||||
>>Select all the vegetables from the list<<
|
||||
|
||||
[ ] Banana {{ selected: No, sorry, a banana is a fruit. }, {unselected: poor banana.}}
|
||||
[ ] Ice Cream
|
||||
[ ] Mushroom {{U: You're right that mushrooms aren't vegetables.}, { selected: Mushroom is a fungus, not a vegetable.}}
|
||||
[x] Brussel Sprout {{S: Brussel sprouts are vegetables.}, {u: Brussel sprout is the only vegetable in this list.}}
|
||||
|
||||
|
||||
{{ ((A*B)) Making a banana split? }}
|
||||
{{ ((B*D)) That will make a horrible dessert: a brussel sprout split? }}\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<label>Select all the fruits from the list</label>
|
||||
<choiceresponse>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">Apple
|
||||
<choicehint selected="true">You're right that apple is a fruit.</choicehint>
|
||||
<choicehint selected="false">Remember that apple is also a fruit.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint selected="true">Mushroom is a fungus, not a fruit.</choicehint>
|
||||
<choicehint selected="false">You're right that mushrooms aren't fruit</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">Grape
|
||||
<choicehint selected="true">You're right that grape is a fruit</choicehint>
|
||||
<choicehint selected="false">Remember that grape is also a fruit.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Mustang</choice>
|
||||
<choice correct="false">Camero
|
||||
<choicehint selected="true">I don't know what a Camero is but it isn't a fruit.</choicehint>
|
||||
<choicehint selected="false">What is a camero anyway?</choicehint>
|
||||
</choice>
|
||||
<compoundhint value="A*B">You're right that apple is a fruit, but there's one you're missing. Also, mushroom is not a fruit.</compoundhint>
|
||||
<compoundhint value="B*C">You're right that grape is a fruit, but there's one you're missing. Also, mushroom is not a fruit.</compoundhint>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
|
||||
<label>Select all the vegetables from the list</label>
|
||||
<choiceresponse>
|
||||
<checkboxgroup>
|
||||
<choice correct="false">Banana
|
||||
<choicehint selected="true">No, sorry, a banana is a fruit.</choicehint>
|
||||
<choicehint selected="false">poor banana.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Ice Cream</choice>
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint selected="true">Mushroom is a fungus, not a vegetable.</choicehint>
|
||||
<choicehint selected="false">You're right that mushrooms aren't vegetables.</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">Brussel Sprout
|
||||
<choicehint selected="true">Brussel sprouts are vegetables.</choicehint>
|
||||
<choicehint selected="false">Brussel sprout is the only vegetable in this list.</choicehint>
|
||||
</choice>
|
||||
<compoundhint value="A*B">Making a banana split?</compoundhint>
|
||||
<compoundhint value="B*D">That will make a horrible dessert: a brussel sprout split?</compoundhint>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml also with demand hints', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>Select all the fruits from the list<<
|
||||
|
||||
[x] Apple {{ selected: You're right that apple is a fruit. }, {unselected: Remember that apple is also a fruit.}}
|
||||
[ ] Mushroom {{U: You're right that mushrooms aren't fruit}, { selected: Mushroom is a fungus, not a fruit.}}
|
||||
[x] Grape {{ selected: You're right that grape is a fruit }, {unselected: Remember that grape is also a fruit.}}
|
||||
[ ] Mustang
|
||||
[ ] Camero {{S:I don't know what a Camero is but it isn't a fruit.},{U:What is a camero anyway?}}
|
||||
|
||||
{{ ((A*B)) You're right that apple is a fruit, but there's one you're missing. Also, mushroom is not a fruit.}}
|
||||
{{ ((B*C)) You're right that grape is a fruit, but there's one you're missing. Also, mushroom is not a fruit.}}
|
||||
|
||||
>>Select all the vegetables from the list<<
|
||||
|
||||
[ ] Banana {{ selected: No, sorry, a banana is a fruit. }, {unselected: poor banana.}}
|
||||
[ ] Ice Cream
|
||||
[ ] Mushroom {{U: You're right that mushrooms aren't vegatbles}, { selected: Mushroom is a fungus, not a vegetable.}}
|
||||
[x] Brussel Sprout {{S: Brussel sprouts are vegetables.}, {u: Brussel sprout is the only vegetable in this list.}}
|
||||
|
||||
{{ ((A*B)) Making a banana split? }}
|
||||
{{ ((B*D)) That will make a horrible dessert: a brussel sprout split? }}
|
||||
|
||||
|| Hint one.||
|
||||
|| Hint two. ||
|
||||
|| Hint three. ||\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<label>Select all the fruits from the list</label>
|
||||
<choiceresponse>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">Apple
|
||||
<choicehint selected="true">You're right that apple is a fruit.</choicehint>
|
||||
<choicehint selected="false">Remember that apple is also a fruit.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint selected="true">Mushroom is a fungus, not a fruit.</choicehint>
|
||||
<choicehint selected="false">You're right that mushrooms aren't fruit</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">Grape
|
||||
<choicehint selected="true">You're right that grape is a fruit</choicehint>
|
||||
<choicehint selected="false">Remember that grape is also a fruit.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Mustang</choice>
|
||||
<choice correct="false">Camero
|
||||
<choicehint selected="true">I don't know what a Camero is but it isn't a fruit.</choicehint>
|
||||
<choicehint selected="false">What is a camero anyway?</choicehint>
|
||||
</choice>
|
||||
<compoundhint value="A*B">You're right that apple is a fruit, but there's one you're missing. Also, mushroom is not a fruit.</compoundhint>
|
||||
<compoundhint value="B*C">You're right that grape is a fruit, but there's one you're missing. Also, mushroom is not a fruit.</compoundhint>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
|
||||
<label>Select all the vegetables from the list</label>
|
||||
<choiceresponse>
|
||||
<checkboxgroup>
|
||||
<choice correct="false">Banana
|
||||
<choicehint selected="true">No, sorry, a banana is a fruit.</choicehint>
|
||||
<choicehint selected="false">poor banana.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Ice Cream</choice>
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint selected="true">Mushroom is a fungus, not a vegetable.</choicehint>
|
||||
<choicehint selected="false">You're right that mushrooms aren't vegatbles</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">Brussel Sprout
|
||||
<choicehint selected="true">Brussel sprouts are vegetables.</choicehint>
|
||||
<choicehint selected="false">Brussel sprout is the only vegetable in this list.</choicehint>
|
||||
</choice>
|
||||
<compoundhint value="A*B">Making a banana split?</compoundhint>
|
||||
<compoundhint value="B*D">That will make a horrible dessert: a brussel sprout split?</compoundhint>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>Hint one.</hint>
|
||||
<hint>Hint two.</hint>
|
||||
<hint>Hint three.</hint>
|
||||
</demandhint>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Markdown to xml extended hint multiple choice', function() {
|
||||
it('produces xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>Select the fruit from the list<<
|
||||
|
||||
() Mushroom {{ Mushroom is a fungus, not a fruit.}}
|
||||
() Potato
|
||||
(x) Apple {{ OUTSTANDING::Apple is indeed a fruit.}}
|
||||
|
||||
>>Select the vegetables from the list<<
|
||||
|
||||
() Mushroom {{ Mushroom is a fungus, not a vegetable.}}
|
||||
(x) Potato {{ Potato is a root vegetable. }}
|
||||
() Apple {{ OOPS::Apple is a fruit.}}\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<label>Select the fruit from the list</label>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint>Mushroom is a fungus, not a fruit.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Potato</choice>
|
||||
<choice correct="true">Apple
|
||||
<choicehint label="OUTSTANDING">Apple is indeed a fruit.</choicehint>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<label>Select the vegetables from the list</label>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint>Mushroom is a fungus, not a vegetable.</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">Potato
|
||||
<choicehint>Potato is a root vegetable.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Apple
|
||||
<choicehint label="OOPS">Apple is a fruit.</choicehint>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with demand hints', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>Select the fruit from the list<<
|
||||
|
||||
() Mushroom {{ Mushroom is a fungus, not a fruit.}}
|
||||
() Potato
|
||||
(x) Apple {{ OUTSTANDING::Apple is indeed a fruit.}}
|
||||
|
||||
|| 0) spaces on previous line. ||
|
||||
|| 1) roses are red. ||
|
||||
|
||||
>>Select the vegetables from the list<<
|
||||
|
||||
() Mushroom {{ Mushroom is a fungus, not a vegetable.}}
|
||||
(x) Potato {{ Potato is a root vegetable. }}
|
||||
() Apple {{ OOPS::Apple is a fruit.}}
|
||||
|
||||
|| 2) where are the lions? ||
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<label>Select the fruit from the list</label>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint>Mushroom is a fungus, not a fruit.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Potato</choice>
|
||||
<choice correct="true">Apple
|
||||
<choicehint label="OUTSTANDING">Apple is indeed a fruit.</choicehint>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<label>Select the vegetables from the list</label>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">Mushroom
|
||||
<choicehint>Mushroom is a fungus, not a vegetable.</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">Potato
|
||||
<choicehint>Potato is a root vegetable.</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">Apple
|
||||
<choicehint label="OOPS">Apple is a fruit.</choicehint>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>0) spaces on previous line.</hint>
|
||||
<hint>1) roses are red.</hint>
|
||||
<hint>2) where are the lions?</hint>
|
||||
</demandhint>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Markdown to xml extended hint text input', function() {
|
||||
it('produces xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`>>In which country would you find the city of Paris?<<
|
||||
= France {{ BRAVO::Viva la France! }}
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<stringresponse answer="France" type="ci">
|
||||
<label>In which country would you find the city of Paris?</label>
|
||||
<correcthint label="BRAVO">Viva la France!</correcthint>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with or=', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`>>Where Paris?<<
|
||||
= France {{ BRAVO::hint1}}
|
||||
or= USA {{ meh::hint2 }}
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<stringresponse answer="France" type="ci">
|
||||
<label>Where Paris?</label>
|
||||
<correcthint label="BRAVO">hint1</correcthint>
|
||||
<additional_answer answer="USA"><correcthint label="meh">hint2</correcthint>
|
||||
</additional_answer>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with not=', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`>>Revenge is a dish best served<<
|
||||
= cold {{khaaaaaan!}}
|
||||
not= warm {{feedback2}}
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<stringresponse answer="cold" type="ci">
|
||||
<label>Revenge is a dish best served</label>
|
||||
<correcthint>khaaaaaan!</correcthint>
|
||||
<stringequalhint answer="warm">feedback2</stringequalhint>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with s=', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`>>q<<
|
||||
s= 2 {{feedback1}}
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<stringresponse answer="2" type="ci">
|
||||
<label>q</label>
|
||||
<correcthint>feedback1</correcthint>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with = and or= and not=', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`>>q<<
|
||||
= aaa
|
||||
or= bbb {{feedback1}}
|
||||
not= no {{feedback2}}
|
||||
or= ccc
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<stringresponse answer="aaa" type="ci">
|
||||
<label>q</label>
|
||||
<additional_answer answer="bbb"><correcthint>feedback1</correcthint>
|
||||
</additional_answer>
|
||||
<stringequalhint answer="no">feedback2</stringequalhint>
|
||||
<additional_answer answer="ccc"/>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with s= and or=', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`>>q<<
|
||||
s= 2 {{feedback1}}
|
||||
or= bbb {{feedback2}}
|
||||
or= ccc
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<stringresponse answer="2" type="ci">
|
||||
<label>q</label>
|
||||
<correcthint>feedback1</correcthint>
|
||||
<additional_answer answer="bbb"><correcthint>feedback2</correcthint>
|
||||
</additional_answer>
|
||||
<additional_answer answer="ccc"/>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with each = making a new question', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>q<<
|
||||
= aaa
|
||||
or= bbb
|
||||
s= ccc\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<label>q</label>
|
||||
<stringresponse answer="aaa" type="ci">
|
||||
<additional_answer answer="bbb"></additional_answer>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
<stringresponse answer="ccc" type="ci">
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with each = making a new question amid blank lines and paragraphs', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
paragraph
|
||||
>>q<<
|
||||
= aaa
|
||||
|
||||
or= bbb
|
||||
s= ccc
|
||||
|
||||
paragraph 2
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<p>paragraph</p>
|
||||
<label>q</label>
|
||||
<stringresponse answer="aaa" type="ci">
|
||||
<additional_answer answer="bbb"></additional_answer>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
<stringresponse answer="ccc" type="ci">
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
<p>paragraph 2</p>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml without a question when or= is just hung out there by itself', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
paragraph
|
||||
>>q<<
|
||||
or= aaa
|
||||
paragraph 2
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<p>paragraph</p>
|
||||
<label>q</label>
|
||||
<p>or= aaa</p>
|
||||
<p>paragraph 2</p>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with each = with feedback making a new question', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>q<<
|
||||
s= aaa
|
||||
or= bbb {{feedback1}}
|
||||
= ccc {{feedback2}}
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<label>q</label>
|
||||
<stringresponse answer="aaa" type="ci">
|
||||
<additional_answer answer="bbb">
|
||||
<correcthint>feedback1</correcthint>
|
||||
</additional_answer>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
<stringresponse answer="ccc" type="ci">
|
||||
<correcthint>feedback2</correcthint>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with demand hints', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`>>Where Paris?<<
|
||||
= France {{ BRAVO::hint1 }}
|
||||
|
||||
|| There are actually two countries with cities named Paris. ||
|
||||
|| Paris is the capital of one of those countries. ||
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<stringresponse answer="France" type="ci">
|
||||
<label>Where Paris?</label>
|
||||
<correcthint label="BRAVO">hint1</correcthint>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>There are actually two countries with cities named Paris.</hint>
|
||||
<hint>Paris is the capital of one of those countries.</hint>
|
||||
</demandhint>
|
||||
</problem>`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Markdown to xml extended hint numeric input', function() {
|
||||
it('produces xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>Enter the numerical value of Pi:<<
|
||||
= 3.14159 +- .02 {{ Pie for everyone! }}
|
||||
|
||||
>>Enter the approximate value of 502*9:<<
|
||||
= 4518 +- 15% {{PIE:: No pie for you!}}
|
||||
|
||||
>>Enter the number of fingers on a human hand<<
|
||||
= 5
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<label>Enter the numerical value of Pi:</label>
|
||||
<numericalresponse answer="3.14159">
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<formulaequationinput/>
|
||||
<correcthint>Pie for everyone!</correcthint>
|
||||
</numericalresponse>
|
||||
|
||||
<label>Enter the approximate value of 502*9:</label>
|
||||
<numericalresponse answer="4518">
|
||||
<responseparam type="tolerance" default="15%"/>
|
||||
<formulaequationinput/>
|
||||
<correcthint label="PIE">No pie for you!</correcthint>
|
||||
</numericalresponse>
|
||||
|
||||
<label>Enter the number of fingers on a human hand</label>
|
||||
<numericalresponse answer="5">
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
// The output xml here shows some of the quirks of how historical markdown parsing does or does not put
|
||||
// in blank lines.
|
||||
it('numeric input with hints and demand hints', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>text1<<
|
||||
= 1 {{ hint1 }}
|
||||
|| hintA ||
|
||||
>>text2<<
|
||||
= 2 {{ hint2 }}
|
||||
|
||||
|| hintB ||
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<label>text1</label>
|
||||
<numericalresponse answer="1">
|
||||
<formulaequationinput/>
|
||||
<correcthint>hint1</correcthint>
|
||||
</numericalresponse>
|
||||
<label>text2</label>
|
||||
<numericalresponse answer="2">
|
||||
<formulaequationinput/>
|
||||
<correcthint>hint2</correcthint>
|
||||
</numericalresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>hintA</hint>
|
||||
<hint>hintB</hint>
|
||||
</demandhint>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Markdown to xml extended hint with multiline hints', () =>
|
||||
it('produces xml', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>Checkboxes<<
|
||||
|
||||
[x] A {{
|
||||
selected: aaa },
|
||||
{unselected:bbb}}
|
||||
[ ] B {{U: c}, {
|
||||
selected: d.}}
|
||||
|
||||
{{ ((A*B)) A*B hint}}
|
||||
|
||||
>>What is 1 + 1?<<
|
||||
= 2 {{ part one, and
|
||||
part two
|
||||
}}
|
||||
|
||||
>>hello?<<
|
||||
= hello {{
|
||||
hello
|
||||
hint
|
||||
}}
|
||||
|
||||
>>multiple choice<<
|
||||
(x) AA{{hint1}}
|
||||
() BB {{
|
||||
hint2
|
||||
}}
|
||||
( ) CC {{ hint3
|
||||
}}
|
||||
|
||||
>>dropdown<<
|
||||
[[
|
||||
W1 {{
|
||||
no }}
|
||||
W2 {{
|
||||
nope}}
|
||||
(C1) {{ yes
|
||||
}}
|
||||
]]
|
||||
|
||||
|| aaa ||
|
||||
||bbb||
|
||||
|| ccc ||
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<label>Checkboxes</label>
|
||||
<choiceresponse>
|
||||
<checkboxgroup>
|
||||
<choice correct="true">A
|
||||
<choicehint selected="true">aaa</choicehint>
|
||||
<choicehint selected="false">bbb</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">B
|
||||
<choicehint selected="true">d.</choicehint>
|
||||
<choicehint selected="false">c</choicehint>
|
||||
</choice>
|
||||
<compoundhint value="A*B">A*B hint</compoundhint>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
|
||||
<label>What is 1 + 1?</label>
|
||||
<numericalresponse answer="2">
|
||||
<formulaequationinput/>
|
||||
<correcthint>part one, and part two</correcthint>
|
||||
</numericalresponse>
|
||||
|
||||
<label>hello?</label>
|
||||
<stringresponse answer="hello" type="ci">
|
||||
<correcthint>hello hint</correcthint>
|
||||
<textline size="20"/>
|
||||
</stringresponse>
|
||||
|
||||
<label>multiple choice</label>
|
||||
<multiplechoiceresponse>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="true">AA
|
||||
<choicehint>hint1</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">BB
|
||||
<choicehint>hint2</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">CC
|
||||
<choicehint>hint3</choicehint>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<label>dropdown</label>
|
||||
<optionresponse>
|
||||
<optioninput>
|
||||
<option correct="False">W1
|
||||
<optionhint>no</optionhint>
|
||||
</option>
|
||||
<option correct="False">W2
|
||||
<optionhint>nope</optionhint>
|
||||
</option>
|
||||
<option correct="True">C1
|
||||
<optionhint>yes</optionhint>
|
||||
</option>
|
||||
</optioninput>
|
||||
</optionresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>aaa</hint>
|
||||
<hint>bbb</hint>
|
||||
<hint>ccc</hint>
|
||||
</demandhint>
|
||||
</problem>\
|
||||
`);
|
||||
})
|
||||
);
|
||||
|
||||
describe('Markdown to xml extended hint with tricky syntax cases', function() {
|
||||
// I'm entering this as utf-8 in this file.
|
||||
// I cannot find a way to set the encoding for .coffee files but it seems to work.
|
||||
it('produces xml with unicode', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>á and Ø<<
|
||||
|
||||
(x) Ø{{Ø}}
|
||||
() BB
|
||||
|
||||
|| Ø ||
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<multiplechoiceresponse>
|
||||
<label>á and Ø</label>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="true">Ø
|
||||
<choicehint>Ø</choicehint>
|
||||
</choice>
|
||||
<choice correct="false">BB</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<demandhint>
|
||||
<hint>Ø</hint>
|
||||
</demandhint>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with quote-type characters', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>"quotes" aren't \`fun\`<<
|
||||
() "hello" {{ isn't }}
|
||||
(x) "isn't" {{ "hello" }}
|
||||
\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<multiplechoiceresponse>
|
||||
<label>"quotes" aren't \`fun\`</label>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">"hello"
|
||||
<choicehint>isn't</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">"isn't"
|
||||
<choicehint>"hello"</choicehint>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
it('produces xml with almost but not quite multiple choice syntax', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>q1<<
|
||||
this (x)
|
||||
() a {{ (hint) }}
|
||||
(x) b
|
||||
that (y)\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<multiplechoiceresponse>
|
||||
<label>q1</label>
|
||||
<p>this (x)</p>
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false">a <choicehint>(hint)</choicehint>
|
||||
</choice>
|
||||
<choice correct="true">b</choice>
|
||||
</choicegroup>
|
||||
<p>that (y)</p>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
// An incomplete checkbox hint passes through to cue the author
|
||||
it('produce xml with almost but not quite checkboxgroup syntax', function() {
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(`\
|
||||
>>q1<<
|
||||
this [x]
|
||||
[ ] a [square]
|
||||
[x] b {{ this hint passes through }}
|
||||
that []\
|
||||
`);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<choiceresponse>
|
||||
<label>q1</label>
|
||||
<p>this [x]</p>
|
||||
<checkboxgroup>
|
||||
<choice correct="false">a [square]</choice>
|
||||
<choice correct="true">b {{ this hint passes through }}</choice>
|
||||
</checkboxgroup>
|
||||
<p>that []</p>
|
||||
</choiceresponse>
|
||||
|
||||
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
|
||||
// It's sort of a pain to edit DOS line endings without some editor or other "fixing" them
|
||||
// for you. Therefore, we construct DOS line endings on the fly just for the test.
|
||||
it('produces xml with DOS \r\n line endings', function() {
|
||||
let markdown = `\
|
||||
>>q22<<
|
||||
|
||||
[[
|
||||
(x) {{ hintx
|
||||
these
|
||||
span
|
||||
}}
|
||||
|
||||
yy {{ meh::hinty }}
|
||||
zzz {{ hintz }}
|
||||
]]\
|
||||
`;
|
||||
markdown = markdown.replace(/\n/g, '\r\n'); // make DOS line endings
|
||||
const data = MarkdownEditingDescriptor.markdownToXml(markdown);
|
||||
expect(data).toXMLEqual(`\
|
||||
<problem>
|
||||
<optionresponse>
|
||||
<label>q22</label>
|
||||
<optioninput>
|
||||
<option correct="True">x <optionhint>hintx these span</optionhint>
|
||||
</option>
|
||||
<option correct="False">yy <optionhint label="meh">hinty</optionhint>
|
||||
</option>
|
||||
<option correct="False">zzz <optionhint>hintz</optionhint>
|
||||
</option>
|
||||
</optioninput>
|
||||
</optionresponse>
|
||||
|
||||
|
||||
</problem>\
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
describe "TabsEditingDescriptor", ->
|
||||
beforeEach ->
|
||||
@isInactiveClass = "is-inactive"
|
||||
@isCurrent = "current"
|
||||
loadFixtures 'tabs-edit.html'
|
||||
@descriptor = new TabsEditingDescriptor($('.xblock'))
|
||||
@html_id = 'test_id'
|
||||
@tab_0_switch = jasmine.createSpy('tab_0_switch');
|
||||
@tab_0_modelUpdate = jasmine.createSpy('tab_0_modelUpdate');
|
||||
@tab_1_switch = jasmine.createSpy('tab_1_switch');
|
||||
@tab_1_modelUpdate = jasmine.createSpy('tab_1_modelUpdate');
|
||||
TabsEditingDescriptor.Model.addModelUpdate(@html_id, 'Tab 0 Editor', @tab_0_modelUpdate)
|
||||
TabsEditingDescriptor.Model.addOnSwitch(@html_id, 'Tab 0 Editor', @tab_0_switch)
|
||||
TabsEditingDescriptor.Model.addModelUpdate(@html_id, 'Tab 1 Transcripts', @tab_1_modelUpdate)
|
||||
TabsEditingDescriptor.Model.addOnSwitch(@html_id, 'Tab 1 Transcripts', @tab_1_switch)
|
||||
|
||||
spyOn($.fn, 'hide').and.callThrough()
|
||||
spyOn($.fn, 'show').and.callThrough()
|
||||
spyOn(TabsEditingDescriptor.Model, 'initialize')
|
||||
spyOn(TabsEditingDescriptor.Model, 'updateValue')
|
||||
|
||||
afterEach ->
|
||||
TabsEditingDescriptor.Model.modules= {}
|
||||
|
||||
describe "constructor", ->
|
||||
it "first tab should be visible", ->
|
||||
expect(@descriptor.$tabs.first()).toHaveClass(@isCurrent)
|
||||
expect(@descriptor.$content.first()).not.toHaveClass(@isInactiveClass)
|
||||
|
||||
describe "onSwitchEditor", ->
|
||||
it "switching tabs changes styles", ->
|
||||
@descriptor.$tabs.eq(1).trigger("click")
|
||||
expect(@descriptor.$tabs.eq(0)).not.toHaveClass(@isCurrent)
|
||||
expect(@descriptor.$content.eq(0)).toHaveClass(@isInactiveClass)
|
||||
expect(@descriptor.$tabs.eq(1)).toHaveClass(@isCurrent)
|
||||
expect(@descriptor.$content.eq(1)).not.toHaveClass(@isInactiveClass)
|
||||
expect(@tab_1_switch).toHaveBeenCalled()
|
||||
|
||||
it "if click on current tab, nothing should happen", ->
|
||||
spyOn($.fn, 'trigger').and.callThrough()
|
||||
currentTab = @descriptor.$tabs.filter('.' + @isCurrent)
|
||||
@descriptor.$tabs.eq(0).trigger("click")
|
||||
expect(@descriptor.$tabs.filter('.' + @isCurrent)).toEqual(currentTab)
|
||||
expect($.fn.trigger.calls.count()).toEqual(1)
|
||||
|
||||
it "onSwitch function call", ->
|
||||
@descriptor.$tabs.eq(1).trigger("click")
|
||||
expect(TabsEditingDescriptor.Model.updateValue).toHaveBeenCalled()
|
||||
expect(@tab_1_switch).toHaveBeenCalled()
|
||||
|
||||
describe "save", ->
|
||||
it "function for current tab should be called", ->
|
||||
@descriptor.$tabs.eq(1).trigger("click")
|
||||
data = @descriptor.save().data
|
||||
expect(@tab_1_modelUpdate).toHaveBeenCalled()
|
||||
|
||||
it "detach click event", ->
|
||||
spyOn($.fn, "off")
|
||||
@descriptor.save()
|
||||
expect($.fn.off).toHaveBeenCalledWith(
|
||||
'click',
|
||||
'.editor-tabs .tab',
|
||||
@descriptor.onSwitchEditor
|
||||
)
|
||||
|
||||
describe "TabsEditingDescriptor special save cases", ->
|
||||
beforeEach ->
|
||||
@isInactiveClass = "is-inactive"
|
||||
@isCurrent = "current"
|
||||
loadFixtures 'tabs-edit.html'
|
||||
@descriptor = new window.TabsEditingDescriptor($('.xblock'))
|
||||
@html_id = 'test_id'
|
||||
|
||||
describe "save", ->
|
||||
it "case: no init", ->
|
||||
data = @descriptor.save().data
|
||||
expect(data).toEqual(null)
|
||||
|
||||
it "case: no function in model update", ->
|
||||
TabsEditingDescriptor.Model.initialize(@html_id)
|
||||
data = @descriptor.save().data
|
||||
expect(data).toEqual(null)
|
||||
|
||||
it "case: no function in model update, but value presented", ->
|
||||
@tab_0_modelUpdate = jasmine.createSpy('tab_0_modelUpdate').and.returnValue(1)
|
||||
TabsEditingDescriptor.Model.addModelUpdate(@html_id, 'Tab 0 Editor', @tab_0_modelUpdate)
|
||||
@descriptor.$tabs.eq(1).trigger("click")
|
||||
expect(@tab_0_modelUpdate).toHaveBeenCalled()
|
||||
data = @descriptor.save().data
|
||||
expect(data).toEqual(1)
|
||||
106
common/lib/xmodule/xmodule/js/spec/tabs/edit.js
Normal file
106
common/lib/xmodule/xmodule/js/spec/tabs/edit.js
Normal file
@@ -0,0 +1,106 @@
|
||||
describe("TabsEditingDescriptor", function() {
|
||||
beforeEach(function() {
|
||||
this.isInactiveClass = "is-inactive";
|
||||
this.isCurrent = "current";
|
||||
loadFixtures('tabs-edit.html');
|
||||
this.descriptor = new TabsEditingDescriptor($('.xblock'));
|
||||
this.html_id = 'test_id';
|
||||
this.tab_0_switch = jasmine.createSpy('tab_0_switch');
|
||||
this.tab_0_modelUpdate = jasmine.createSpy('tab_0_modelUpdate');
|
||||
this.tab_1_switch = jasmine.createSpy('tab_1_switch');
|
||||
this.tab_1_modelUpdate = jasmine.createSpy('tab_1_modelUpdate');
|
||||
TabsEditingDescriptor.Model.addModelUpdate(this.html_id, 'Tab 0 Editor', this.tab_0_modelUpdate);
|
||||
TabsEditingDescriptor.Model.addOnSwitch(this.html_id, 'Tab 0 Editor', this.tab_0_switch);
|
||||
TabsEditingDescriptor.Model.addModelUpdate(this.html_id, 'Tab 1 Transcripts', this.tab_1_modelUpdate);
|
||||
TabsEditingDescriptor.Model.addOnSwitch(this.html_id, 'Tab 1 Transcripts', this.tab_1_switch);
|
||||
|
||||
spyOn($.fn, 'hide').and.callThrough();
|
||||
spyOn($.fn, 'show').and.callThrough();
|
||||
spyOn(TabsEditingDescriptor.Model, 'initialize');
|
||||
spyOn(TabsEditingDescriptor.Model, 'updateValue');
|
||||
});
|
||||
|
||||
afterEach(() => TabsEditingDescriptor.Model.modules= {});
|
||||
|
||||
describe("constructor", () =>
|
||||
it("first tab should be visible", function() {
|
||||
expect(this.descriptor.$tabs.first()).toHaveClass(this.isCurrent);
|
||||
expect(this.descriptor.$content.first()).not.toHaveClass(this.isInactiveClass);
|
||||
})
|
||||
);
|
||||
|
||||
describe("onSwitchEditor", function() {
|
||||
it("switching tabs changes styles", function() {
|
||||
this.descriptor.$tabs.eq(1).trigger("click");
|
||||
expect(this.descriptor.$tabs.eq(0)).not.toHaveClass(this.isCurrent);
|
||||
expect(this.descriptor.$content.eq(0)).toHaveClass(this.isInactiveClass);
|
||||
expect(this.descriptor.$tabs.eq(1)).toHaveClass(this.isCurrent);
|
||||
expect(this.descriptor.$content.eq(1)).not.toHaveClass(this.isInactiveClass);
|
||||
expect(this.tab_1_switch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("if click on current tab, nothing should happen", function() {
|
||||
spyOn($.fn, 'trigger').and.callThrough();
|
||||
const currentTab = this.descriptor.$tabs.filter(`.${this.isCurrent}`);
|
||||
this.descriptor.$tabs.eq(0).trigger("click");
|
||||
expect(this.descriptor.$tabs.filter(`.${this.isCurrent}`)).toEqual(currentTab);
|
||||
expect($.fn.trigger.calls.count()).toEqual(1);
|
||||
});
|
||||
|
||||
it("onSwitch function call", function() {
|
||||
this.descriptor.$tabs.eq(1).trigger("click");
|
||||
expect(TabsEditingDescriptor.Model.updateValue).toHaveBeenCalled();
|
||||
expect(this.tab_1_switch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("save", function() {
|
||||
it("function for current tab should be called", function() {
|
||||
this.descriptor.$tabs.eq(1).trigger("click");
|
||||
const { data } = this.descriptor.save();
|
||||
expect(this.tab_1_modelUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("detach click event", function() {
|
||||
spyOn($.fn, "off");
|
||||
this.descriptor.save();
|
||||
expect($.fn.off).toHaveBeenCalledWith(
|
||||
'click',
|
||||
'.editor-tabs .tab',
|
||||
this.descriptor.onSwitchEditor
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("TabsEditingDescriptor special save cases", function() {
|
||||
beforeEach(function() {
|
||||
this.isInactiveClass = "is-inactive";
|
||||
this.isCurrent = "current";
|
||||
loadFixtures('tabs-edit.html');
|
||||
this.descriptor = new window.TabsEditingDescriptor($('.xblock'));
|
||||
this.html_id = 'test_id';
|
||||
});
|
||||
|
||||
describe("save", function() {
|
||||
it("case: no init", function() {
|
||||
const { data } = this.descriptor.save();
|
||||
expect(data).toEqual(null);
|
||||
});
|
||||
|
||||
it("case: no function in model update", function() {
|
||||
TabsEditingDescriptor.Model.initialize(this.html_id);
|
||||
const { data } = this.descriptor.save();
|
||||
expect(data).toEqual(null);
|
||||
});
|
||||
|
||||
it("case: no function in model update, but value presented", function() {
|
||||
this.tab_0_modelUpdate = jasmine.createSpy('tab_0_modelUpdate').and.returnValue(1);
|
||||
TabsEditingDescriptor.Model.addModelUpdate(this.html_id, 'Tab 0 Editor', this.tab_0_modelUpdate);
|
||||
this.descriptor.$tabs.eq(1).trigger("click");
|
||||
expect(this.tab_0_modelUpdate).toHaveBeenCalled();
|
||||
const { data } = this.descriptor.save();
|
||||
expect(data).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
1
common/static/coffee/spec/.gitignore
vendored
1
common/static/coffee/spec/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
*.js
|
||||
@@ -1,26 +0,0 @@
|
||||
describe "$.immediateDescendents", ->
|
||||
beforeEach ->
|
||||
setFixtures """
|
||||
<div>
|
||||
<div class='xblock' id='child'>
|
||||
<div class='xblock' id='nested'/>
|
||||
</div>
|
||||
<div>
|
||||
<div class='xblock' id='grandchild'/>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
@descendents = $('#jasmine-fixtures').immediateDescendents(".xblock").get()
|
||||
|
||||
it "finds non-immediate children", ->
|
||||
expect(@descendents).toContain($('#grandchild').get(0))
|
||||
|
||||
it "finds immediate children", ->
|
||||
expect(@descendents).toContain($('#child').get(0))
|
||||
|
||||
it "skips nested descendents", ->
|
||||
expect(@descendents).not.toContain($('#nested').get(0))
|
||||
|
||||
it "finds 2 children", ->
|
||||
expect(@descendents.length).toBe(2)
|
||||
@@ -0,0 +1,33 @@
|
||||
describe("$.immediateDescendents", function() {
|
||||
beforeEach(function() {
|
||||
setFixtures(`\
|
||||
<div>
|
||||
<div class='xblock' id='child'>
|
||||
<div class='xblock' id='nested'/>
|
||||
</div>
|
||||
<div>
|
||||
<div class='xblock' id='grandchild'/>
|
||||
</div>
|
||||
</div>\
|
||||
`
|
||||
);
|
||||
|
||||
this.descendents = $('#jasmine-fixtures').immediateDescendents(".xblock").get();
|
||||
});
|
||||
|
||||
it("finds non-immediate children", function() {
|
||||
expect(this.descendents).toContain($('#grandchild').get(0));
|
||||
});
|
||||
|
||||
it("finds immediate children", function() {
|
||||
expect(this.descendents).toContain($('#child').get(0));
|
||||
});
|
||||
|
||||
it("skips nested descendents", function() {
|
||||
expect(this.descendents).not.toContain($('#nested').get(0));
|
||||
});
|
||||
|
||||
it("finds 2 children", function() {
|
||||
expect(this.descendents.length).toBe(2);
|
||||
});
|
||||
});
|
||||
1
lms/static/coffee/.gitignore
vendored
1
lms/static/coffee/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
*.js
|
||||
@@ -1,353 +0,0 @@
|
||||
describe 'Calculator', ->
|
||||
|
||||
KEY =
|
||||
TAB : 9
|
||||
ENTER : 13
|
||||
ALT : 18
|
||||
ESC : 27
|
||||
SPACE : 32
|
||||
LEFT : 37
|
||||
UP : 38
|
||||
RIGHT : 39
|
||||
DOWN : 40
|
||||
|
||||
beforeEach ->
|
||||
loadFixtures 'coffee/fixtures/calculator.html'
|
||||
@calculator = new Calculator
|
||||
|
||||
describe 'bind', ->
|
||||
it 'bind the calculator button', ->
|
||||
expect($('.calc')).toHandleWith 'click', @calculator.toggle
|
||||
|
||||
it 'bind key up on calculator', ->
|
||||
expect($('#calculator_wrapper')).toHandle 'keyup', @calculator.handleKeyUpOnHint
|
||||
|
||||
it 'bind the help button', ->
|
||||
# This events is bind by $.click()
|
||||
expect($('#calculator_hint')).toHandle 'click'
|
||||
|
||||
it 'bind the calculator submit', ->
|
||||
expect($('form#calculator')).toHandleWith 'submit', @calculator.calculate
|
||||
|
||||
xit 'prevent default behavior on form submit', ->
|
||||
jasmine.stubRequests()
|
||||
$('form#calculator').submit (e) ->
|
||||
expect(e.isDefaultPrevented()).toBeTruthy()
|
||||
e.preventDefault()
|
||||
$('form#calculator').submit()
|
||||
|
||||
describe 'toggle', ->
|
||||
it 'focuses the input when toggled', (done)->
|
||||
|
||||
self = this
|
||||
focus = ()->
|
||||
deferred = $.Deferred()
|
||||
|
||||
# Since the focus is called asynchronously, we need to
|
||||
# wait until focus() is called.
|
||||
spyOn($.fn, 'focus').and.callFake (elementName) ->
|
||||
deferred.resolve()
|
||||
|
||||
self.calculator.toggle(jQuery.Event("click"))
|
||||
|
||||
deferred.promise()
|
||||
|
||||
focus().then(
|
||||
->
|
||||
expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled()
|
||||
).always(done)
|
||||
|
||||
it 'toggle the close button on the calculator button', ->
|
||||
@calculator.toggle(jQuery.Event("click"))
|
||||
expect($('.calc')).toHaveClass('closed')
|
||||
|
||||
@calculator.toggle(jQuery.Event("click"))
|
||||
expect($('.calc')).not.toHaveClass('closed')
|
||||
|
||||
describe 'showHint', ->
|
||||
it 'show the help overlay', ->
|
||||
@calculator.showHint()
|
||||
expect($('.help')).toHaveClass('shown')
|
||||
expect($('.help')).toHaveAttr('aria-hidden', 'false')
|
||||
|
||||
|
||||
describe 'hideHint', ->
|
||||
it 'show the help overlay', ->
|
||||
@calculator.hideHint()
|
||||
expect($('.help')).not.toHaveClass('shown')
|
||||
expect($('.help')).toHaveAttr('aria-hidden', 'true')
|
||||
|
||||
describe 'handleClickOnHintButton', ->
|
||||
it 'on click hint button hint popup becomes visible ', ->
|
||||
e = jQuery.Event('click');
|
||||
$('#calculator_hint').trigger(e);
|
||||
expect($('.help')).toHaveClass 'shown'
|
||||
|
||||
describe 'handleClickOnDocument', ->
|
||||
it 'on click out of the hint popup it becomes hidden', ->
|
||||
@calculator.showHint()
|
||||
e = jQuery.Event('click');
|
||||
$(document).trigger(e);
|
||||
expect($('.help')).not.toHaveClass 'shown'
|
||||
|
||||
describe 'handleClickOnHintPopup', ->
|
||||
it 'on click of hint popup it remains visible', ->
|
||||
@calculator.showHint()
|
||||
e = jQuery.Event('click');
|
||||
$('#calculator_input_help').trigger(e);
|
||||
expect($('.help')).toHaveClass 'shown'
|
||||
|
||||
describe 'selectHint', ->
|
||||
it 'select correct hint item', ->
|
||||
spyOn($.fn, 'focus')
|
||||
element = $('.hint-item').eq(1)
|
||||
@calculator.selectHint(element)
|
||||
|
||||
expect(element.focus).toHaveBeenCalled()
|
||||
expect(@calculator.activeHint).toEqual(element)
|
||||
expect(@calculator.hintPopup).toHaveAttr('data-calculator-hint', element.attr('id'))
|
||||
|
||||
it 'select the first hint if argument element is not passed', ->
|
||||
@calculator.selectHint()
|
||||
expect(@calculator.activeHint.attr('id')).toEqual($('.hint-item').first().attr('id'))
|
||||
|
||||
it 'select the first hint if argument element is empty', ->
|
||||
@calculator.selectHint([])
|
||||
expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').first().attr('id'))
|
||||
|
||||
describe 'prevHint', ->
|
||||
|
||||
it 'Prev hint item is selected', ->
|
||||
@calculator.activeHint = $('.hint-item').eq(1)
|
||||
@calculator.prevHint()
|
||||
|
||||
expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(0).attr('id'))
|
||||
|
||||
it 'if this was the second item, select the first one', ->
|
||||
@calculator.activeHint = $('.hint-item').eq(1)
|
||||
@calculator.prevHint()
|
||||
|
||||
expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(0).attr('id'))
|
||||
|
||||
it 'if this was the first item, select the last one', ->
|
||||
@calculator.activeHint = $('.hint-item').eq(0)
|
||||
@calculator.prevHint()
|
||||
|
||||
expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(2).attr('id'))
|
||||
|
||||
it 'if this was the last item, select the second last', ->
|
||||
@calculator.activeHint = $('.hint-item').eq(2)
|
||||
@calculator.prevHint()
|
||||
|
||||
expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(1).attr('id'))
|
||||
|
||||
describe 'nextHint', ->
|
||||
|
||||
it 'if this was the first item, select the second one', ->
|
||||
@calculator.activeHint = $('.hint-item').eq(0)
|
||||
@calculator.nextHint()
|
||||
|
||||
expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(1).attr('id'))
|
||||
|
||||
it 'If this was the second item, select the last one', ->
|
||||
@calculator.activeHint = $('.hint-item').eq(1)
|
||||
@calculator.nextHint()
|
||||
|
||||
expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(2).attr('id'))
|
||||
|
||||
it 'If this was the last item, select the first one', ->
|
||||
@calculator.activeHint = $('.hint-item').eq(2)
|
||||
@calculator.nextHint()
|
||||
|
||||
expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(0).attr('id'))
|
||||
|
||||
describe 'handleKeyDown', ->
|
||||
assertHintIsHidden = (calc, key) ->
|
||||
spyOn(calc, 'hideHint')
|
||||
calc.showHint()
|
||||
e = jQuery.Event('keydown', { keyCode: key });
|
||||
value = calc.handleKeyDown(e)
|
||||
|
||||
expect(calc.hideHint).toHaveBeenCalled
|
||||
expect(value).toBeFalsy()
|
||||
expect(e.isDefaultPrevented()).toBeTruthy()
|
||||
|
||||
assertHintIsVisible = (calc, key) ->
|
||||
spyOn(calc, 'showHint')
|
||||
spyOn($.fn, 'focus')
|
||||
e = jQuery.Event('keydown', { keyCode: key });
|
||||
value = calc.handleKeyDown(e)
|
||||
|
||||
expect(calc.showHint).toHaveBeenCalled
|
||||
expect(value).toBeFalsy()
|
||||
expect(e.isDefaultPrevented()).toBeTruthy()
|
||||
expect(calc.activeHint.focus).toHaveBeenCalled()
|
||||
|
||||
assertNothingHappens = (calc, key) ->
|
||||
spyOn(calc, 'showHint')
|
||||
e = jQuery.Event('keydown', { keyCode: key });
|
||||
value = calc.handleKeyDown(e)
|
||||
|
||||
expect(calc.showHint).not.toHaveBeenCalled
|
||||
expect(value).toBeTruthy()
|
||||
expect(e.isDefaultPrevented()).toBeFalsy()
|
||||
|
||||
it 'hint popup becomes hidden on press ENTER', ->
|
||||
assertHintIsHidden(@calculator, KEY.ENTER)
|
||||
|
||||
it 'hint popup becomes visible on press ENTER', ->
|
||||
assertHintIsVisible(@calculator, KEY.ENTER)
|
||||
|
||||
it 'hint popup becomes hidden on press SPACE', ->
|
||||
assertHintIsHidden(@calculator, KEY.SPACE)
|
||||
|
||||
it 'hint popup becomes visible on press SPACE', ->
|
||||
assertHintIsVisible(@calculator, KEY.SPACE)
|
||||
|
||||
it 'Nothing happens on press ALT', ->
|
||||
assertNothingHappens(@calculator, KEY.ALT)
|
||||
|
||||
it 'Nothing happens on press any other button', ->
|
||||
assertNothingHappens(@calculator, KEY.DOWN)
|
||||
|
||||
describe 'handleKeyDownOnHint', ->
|
||||
it 'Navigation works in proper way', ->
|
||||
calc = @calculator
|
||||
|
||||
eventToShowHint = jQuery.Event('keydown', { keyCode: KEY.ENTER } );
|
||||
$('#calculator_hint').trigger(eventToShowHint);
|
||||
|
||||
spyOn(calc, 'hideHint')
|
||||
spyOn(calc, 'prevHint')
|
||||
spyOn(calc, 'nextHint')
|
||||
spyOn($.fn, 'focus')
|
||||
|
||||
cases =
|
||||
left:
|
||||
event:
|
||||
keyCode: KEY.LEFT
|
||||
shiftKey: false
|
||||
returnedValue: false
|
||||
called:
|
||||
'prevHint': calc
|
||||
isPropagationStopped: true
|
||||
|
||||
leftWithShift:
|
||||
returnedValue: true
|
||||
event:
|
||||
keyCode: KEY.LEFT
|
||||
shiftKey: true
|
||||
not_called:
|
||||
'prevHint': calc
|
||||
|
||||
up:
|
||||
event:
|
||||
keyCode: KEY.UP
|
||||
shiftKey: false
|
||||
returnedValue: false
|
||||
called:
|
||||
'prevHint': calc
|
||||
isPropagationStopped: true
|
||||
|
||||
upWithShift:
|
||||
returnedValue: true
|
||||
event:
|
||||
keyCode: KEY.UP
|
||||
shiftKey: true
|
||||
not_called:
|
||||
'prevHint': calc
|
||||
|
||||
right:
|
||||
event:
|
||||
keyCode: KEY.RIGHT
|
||||
shiftKey: false
|
||||
returnedValue: false
|
||||
called:
|
||||
'nextHint': calc
|
||||
isPropagationStopped: true
|
||||
|
||||
rightWithShift:
|
||||
returnedValue: true
|
||||
event:
|
||||
keyCode: KEY.RIGHT
|
||||
shiftKey: true
|
||||
not_called:
|
||||
'nextHint': calc
|
||||
|
||||
down:
|
||||
event:
|
||||
keyCode: KEY.DOWN
|
||||
shiftKey: false
|
||||
returnedValue: false
|
||||
called:
|
||||
'nextHint': calc
|
||||
isPropagationStopped: true
|
||||
|
||||
downWithShift:
|
||||
returnedValue: true
|
||||
event:
|
||||
keyCode: KEY.DOWN
|
||||
shiftKey: true
|
||||
not_called:
|
||||
'nextHint': calc
|
||||
|
||||
esc:
|
||||
returnedValue: false
|
||||
event:
|
||||
keyCode: KEY.ESC
|
||||
shiftKey: false
|
||||
called:
|
||||
'hideHint': calc
|
||||
'focus': $.fn
|
||||
isPropagationStopped: true
|
||||
|
||||
alt:
|
||||
returnedValue: true
|
||||
event:
|
||||
which: KEY.ALT
|
||||
not_called:
|
||||
'hideHint': calc
|
||||
'nextHint': calc
|
||||
'prevHint': calc
|
||||
|
||||
$.each(cases, (key, data) ->
|
||||
calc.hideHint.calls.reset()
|
||||
calc.prevHint.calls.reset()
|
||||
calc.nextHint.calls.reset()
|
||||
$.fn.focus.calls.reset()
|
||||
|
||||
e = jQuery.Event('keydown', data.event or {});
|
||||
value = calc.handleKeyDownOnHint(e)
|
||||
|
||||
if data.called
|
||||
$.each(data.called, (method, obj) ->
|
||||
expect(obj[method]).toHaveBeenCalled()
|
||||
)
|
||||
|
||||
if data.not_called
|
||||
$.each(data.not_called, (method, obj) ->
|
||||
expect(obj[method]).not.toHaveBeenCalled()
|
||||
)
|
||||
|
||||
if data.isPropagationStopped
|
||||
expect(e.isPropagationStopped()).toBeTruthy()
|
||||
else
|
||||
expect(e.isPropagationStopped()).toBeFalsy()
|
||||
|
||||
expect(value).toBe(data.returnedValue)
|
||||
)
|
||||
|
||||
describe 'calculate', ->
|
||||
beforeEach ->
|
||||
$('#calculator_input').val '1+2'
|
||||
spyOn($, 'getWithPrefix').and.callFake (url, data, callback) ->
|
||||
callback({ result: 3 })
|
||||
@calculator.calculate()
|
||||
|
||||
it 'send data to /calculate', ->
|
||||
expect($.getWithPrefix).toHaveBeenCalledWith '/calculate',
|
||||
equation: '1+2'
|
||||
, jasmine.any(Function)
|
||||
|
||||
it 'update the calculator output', ->
|
||||
expect($('#calculator_output').val()).toEqual('3')
|
||||
430
lms/static/coffee/spec/calculator_spec.js
Normal file
430
lms/static/coffee/spec/calculator_spec.js
Normal file
@@ -0,0 +1,430 @@
|
||||
describe('Calculator', function() {
|
||||
|
||||
const KEY = {
|
||||
TAB : 9,
|
||||
ENTER : 13,
|
||||
ALT : 18,
|
||||
ESC : 27,
|
||||
SPACE : 32,
|
||||
LEFT : 37,
|
||||
UP : 38,
|
||||
RIGHT : 39,
|
||||
DOWN : 40
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
loadFixtures('coffee/fixtures/calculator.html');
|
||||
this.calculator = new Calculator;
|
||||
});
|
||||
|
||||
describe('bind', function() {
|
||||
it('bind the calculator button', function() {
|
||||
expect($('.calc')).toHandleWith('click', this.calculator.toggle);
|
||||
});
|
||||
|
||||
it('bind key up on calculator', function() {
|
||||
expect($('#calculator_wrapper')).toHandle('keyup', this.calculator.handleKeyUpOnHint);
|
||||
});
|
||||
|
||||
it('bind the help button', () =>
|
||||
// This events is bind by $.click()
|
||||
expect($('#calculator_hint')).toHandle('click')
|
||||
);
|
||||
|
||||
it('bind the calculator submit', function() {
|
||||
expect($('form#calculator')).toHandleWith('submit', this.calculator.calculate);
|
||||
});
|
||||
|
||||
xit('prevent default behavior on form submit', function() {
|
||||
jasmine.stubRequests();
|
||||
$('form#calculator').submit(function(e) {
|
||||
expect(e.isDefaultPrevented()).toBeTruthy();
|
||||
return e.preventDefault();
|
||||
});
|
||||
return $('form#calculator').submit();
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggle', function() {
|
||||
it('focuses the input when toggled', function(done){
|
||||
|
||||
const self = this;
|
||||
const focus = function(){
|
||||
const deferred = $.Deferred();
|
||||
|
||||
// Since the focus is called asynchronously, we need to
|
||||
// wait until focus() is called.
|
||||
spyOn($.fn, 'focus').and.callFake(elementName => deferred.resolve());
|
||||
|
||||
self.calculator.toggle(jQuery.Event("click"));
|
||||
|
||||
return deferred.promise();
|
||||
};
|
||||
|
||||
focus().then(
|
||||
() => expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled()).always(done);
|
||||
});
|
||||
|
||||
it('toggle the close button on the calculator button', function() {
|
||||
this.calculator.toggle(jQuery.Event("click"));
|
||||
expect($('.calc')).toHaveClass('closed');
|
||||
|
||||
this.calculator.toggle(jQuery.Event("click"));
|
||||
expect($('.calc')).not.toHaveClass('closed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('showHint', () =>
|
||||
it('show the help overlay', function() {
|
||||
this.calculator.showHint();
|
||||
expect($('.help')).toHaveClass('shown');
|
||||
expect($('.help')).toHaveAttr('aria-hidden', 'false');
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
describe('hideHint', () =>
|
||||
it('show the help overlay', function() {
|
||||
this.calculator.hideHint();
|
||||
expect($('.help')).not.toHaveClass('shown');
|
||||
expect($('.help')).toHaveAttr('aria-hidden', 'true');
|
||||
})
|
||||
);
|
||||
|
||||
describe('handleClickOnHintButton', () =>
|
||||
it('on click hint button hint popup becomes visible ', function() {
|
||||
const e = jQuery.Event('click');
|
||||
$('#calculator_hint').trigger(e);
|
||||
expect($('.help')).toHaveClass('shown');
|
||||
})
|
||||
);
|
||||
|
||||
describe('handleClickOnDocument', () =>
|
||||
it('on click out of the hint popup it becomes hidden', function() {
|
||||
this.calculator.showHint();
|
||||
const e = jQuery.Event('click');
|
||||
$(document).trigger(e);
|
||||
expect($('.help')).not.toHaveClass('shown');
|
||||
})
|
||||
);
|
||||
|
||||
describe('handleClickOnHintPopup', () =>
|
||||
it('on click of hint popup it remains visible', function() {
|
||||
this.calculator.showHint();
|
||||
const e = jQuery.Event('click');
|
||||
$('#calculator_input_help').trigger(e);
|
||||
expect($('.help')).toHaveClass('shown');
|
||||
})
|
||||
);
|
||||
|
||||
describe('selectHint', function() {
|
||||
it('select correct hint item', function() {
|
||||
spyOn($.fn, 'focus');
|
||||
const element = $('.hint-item').eq(1);
|
||||
this.calculator.selectHint(element);
|
||||
|
||||
expect(element.focus).toHaveBeenCalled();
|
||||
expect(this.calculator.activeHint).toEqual(element);
|
||||
expect(this.calculator.hintPopup).toHaveAttr('data-calculator-hint', element.attr('id'));
|
||||
});
|
||||
|
||||
it('select the first hint if argument element is not passed', function() {
|
||||
this.calculator.selectHint();
|
||||
expect(this.calculator.activeHint.attr('id')).toEqual($('.hint-item').first().attr('id'));
|
||||
});
|
||||
|
||||
it('select the first hint if argument element is empty', function() {
|
||||
this.calculator.selectHint([]);
|
||||
expect(this.calculator.activeHint.attr('id')).toBe($('.hint-item').first().attr('id'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('prevHint', function() {
|
||||
|
||||
it('Prev hint item is selected', function() {
|
||||
this.calculator.activeHint = $('.hint-item').eq(1);
|
||||
this.calculator.prevHint();
|
||||
|
||||
expect(this.calculator.activeHint.attr('id')).toBe($('.hint-item').eq(0).attr('id'));
|
||||
});
|
||||
|
||||
it('if this was the second item, select the first one', function() {
|
||||
this.calculator.activeHint = $('.hint-item').eq(1);
|
||||
this.calculator.prevHint();
|
||||
|
||||
expect(this.calculator.activeHint.attr('id')).toBe($('.hint-item').eq(0).attr('id'));
|
||||
});
|
||||
|
||||
it('if this was the first item, select the last one', function() {
|
||||
this.calculator.activeHint = $('.hint-item').eq(0);
|
||||
this.calculator.prevHint();
|
||||
|
||||
expect(this.calculator.activeHint.attr('id')).toBe($('.hint-item').eq(2).attr('id'));
|
||||
});
|
||||
|
||||
it('if this was the last item, select the second last', function() {
|
||||
this.calculator.activeHint = $('.hint-item').eq(2);
|
||||
this.calculator.prevHint();
|
||||
|
||||
expect(this.calculator.activeHint.attr('id')).toBe($('.hint-item').eq(1).attr('id'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('nextHint', function() {
|
||||
|
||||
it('if this was the first item, select the second one', function() {
|
||||
this.calculator.activeHint = $('.hint-item').eq(0);
|
||||
this.calculator.nextHint();
|
||||
|
||||
expect(this.calculator.activeHint.attr('id')).toBe($('.hint-item').eq(1).attr('id'));
|
||||
});
|
||||
|
||||
it('If this was the second item, select the last one', function() {
|
||||
this.calculator.activeHint = $('.hint-item').eq(1);
|
||||
this.calculator.nextHint();
|
||||
|
||||
expect(this.calculator.activeHint.attr('id')).toBe($('.hint-item').eq(2).attr('id'));
|
||||
});
|
||||
|
||||
it('If this was the last item, select the first one', function() {
|
||||
this.calculator.activeHint = $('.hint-item').eq(2);
|
||||
this.calculator.nextHint();
|
||||
|
||||
expect(this.calculator.activeHint.attr('id')).toBe($('.hint-item').eq(0).attr('id'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleKeyDown', function() {
|
||||
const assertHintIsHidden = function(calc, key) {
|
||||
spyOn(calc, 'hideHint');
|
||||
calc.showHint();
|
||||
const e = jQuery.Event('keydown', { keyCode: key });
|
||||
const value = calc.handleKeyDown(e);
|
||||
|
||||
expect(calc.hideHint).toHaveBeenCalled;
|
||||
expect(value).toBeFalsy();
|
||||
expect(e.isDefaultPrevented()).toBeTruthy();
|
||||
};
|
||||
|
||||
const assertHintIsVisible = function(calc, key) {
|
||||
spyOn(calc, 'showHint');
|
||||
spyOn($.fn, 'focus');
|
||||
const e = jQuery.Event('keydown', { keyCode: key });
|
||||
const value = calc.handleKeyDown(e);
|
||||
|
||||
expect(calc.showHint).toHaveBeenCalled;
|
||||
expect(value).toBeFalsy();
|
||||
expect(e.isDefaultPrevented()).toBeTruthy();
|
||||
expect(calc.activeHint.focus).toHaveBeenCalled();
|
||||
};
|
||||
|
||||
const assertNothingHappens = function(calc, key) {
|
||||
spyOn(calc, 'showHint');
|
||||
const e = jQuery.Event('keydown', { keyCode: key });
|
||||
const value = calc.handleKeyDown(e);
|
||||
|
||||
expect(calc.showHint).not.toHaveBeenCalled;
|
||||
expect(value).toBeTruthy();
|
||||
expect(e.isDefaultPrevented()).toBeFalsy();
|
||||
};
|
||||
|
||||
it('hint popup becomes hidden on press ENTER', function() {
|
||||
assertHintIsHidden(this.calculator, KEY.ENTER);
|
||||
});
|
||||
|
||||
it('hint popup becomes visible on press ENTER', function() {
|
||||
assertHintIsVisible(this.calculator, KEY.ENTER);
|
||||
});
|
||||
|
||||
it('hint popup becomes hidden on press SPACE', function() {
|
||||
assertHintIsHidden(this.calculator, KEY.SPACE);
|
||||
});
|
||||
|
||||
it('hint popup becomes visible on press SPACE', function() {
|
||||
assertHintIsVisible(this.calculator, KEY.SPACE);
|
||||
});
|
||||
|
||||
it('Nothing happens on press ALT', function() {
|
||||
assertNothingHappens(this.calculator, KEY.ALT);
|
||||
});
|
||||
|
||||
it('Nothing happens on press any other button', function() {
|
||||
assertNothingHappens(this.calculator, KEY.DOWN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleKeyDownOnHint', () =>
|
||||
it('Navigation works in proper way', function() {
|
||||
const calc = this.calculator;
|
||||
|
||||
const eventToShowHint = jQuery.Event('keydown', { keyCode: KEY.ENTER } );
|
||||
$('#calculator_hint').trigger(eventToShowHint);
|
||||
|
||||
spyOn(calc, 'hideHint');
|
||||
spyOn(calc, 'prevHint');
|
||||
spyOn(calc, 'nextHint');
|
||||
spyOn($.fn, 'focus');
|
||||
|
||||
const cases = {
|
||||
left: {
|
||||
event: {
|
||||
keyCode: KEY.LEFT,
|
||||
shiftKey: false
|
||||
},
|
||||
returnedValue: false,
|
||||
called: {
|
||||
'prevHint': calc
|
||||
},
|
||||
isPropagationStopped: true
|
||||
},
|
||||
|
||||
leftWithShift: {
|
||||
returnedValue: true,
|
||||
event: {
|
||||
keyCode: KEY.LEFT,
|
||||
shiftKey: true
|
||||
},
|
||||
not_called: {
|
||||
'prevHint': calc
|
||||
}
|
||||
},
|
||||
|
||||
up: {
|
||||
event: {
|
||||
keyCode: KEY.UP,
|
||||
shiftKey: false
|
||||
},
|
||||
returnedValue: false,
|
||||
called: {
|
||||
'prevHint': calc
|
||||
},
|
||||
isPropagationStopped: true
|
||||
},
|
||||
|
||||
upWithShift: {
|
||||
returnedValue: true,
|
||||
event: {
|
||||
keyCode: KEY.UP,
|
||||
shiftKey: true
|
||||
},
|
||||
not_called: {
|
||||
'prevHint': calc
|
||||
}
|
||||
},
|
||||
|
||||
right: {
|
||||
event: {
|
||||
keyCode: KEY.RIGHT,
|
||||
shiftKey: false
|
||||
},
|
||||
returnedValue: false,
|
||||
called: {
|
||||
'nextHint': calc
|
||||
},
|
||||
isPropagationStopped: true
|
||||
},
|
||||
|
||||
rightWithShift: {
|
||||
returnedValue: true,
|
||||
event: {
|
||||
keyCode: KEY.RIGHT,
|
||||
shiftKey: true
|
||||
},
|
||||
not_called: {
|
||||
'nextHint': calc
|
||||
}
|
||||
},
|
||||
|
||||
down: {
|
||||
event: {
|
||||
keyCode: KEY.DOWN,
|
||||
shiftKey: false
|
||||
},
|
||||
returnedValue: false,
|
||||
called: {
|
||||
'nextHint': calc
|
||||
},
|
||||
isPropagationStopped: true
|
||||
},
|
||||
|
||||
downWithShift: {
|
||||
returnedValue: true,
|
||||
event: {
|
||||
keyCode: KEY.DOWN,
|
||||
shiftKey: true
|
||||
},
|
||||
not_called: {
|
||||
'nextHint': calc
|
||||
}
|
||||
},
|
||||
|
||||
esc: {
|
||||
returnedValue: false,
|
||||
event: {
|
||||
keyCode: KEY.ESC,
|
||||
shiftKey: false
|
||||
},
|
||||
called: {
|
||||
'hideHint': calc,
|
||||
'focus': $.fn
|
||||
},
|
||||
isPropagationStopped: true
|
||||
},
|
||||
|
||||
alt: {
|
||||
returnedValue: true,
|
||||
event: {
|
||||
which: KEY.ALT
|
||||
},
|
||||
not_called: {
|
||||
'hideHint': calc,
|
||||
'nextHint': calc,
|
||||
'prevHint': calc
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.each(cases, function(key, data) {
|
||||
calc.hideHint.calls.reset();
|
||||
calc.prevHint.calls.reset();
|
||||
calc.nextHint.calls.reset();
|
||||
$.fn.focus.calls.reset();
|
||||
|
||||
const e = jQuery.Event('keydown', data.event || {});
|
||||
const value = calc.handleKeyDownOnHint(e);
|
||||
|
||||
if (data.called) {
|
||||
$.each(data.called, (method, obj) => expect(obj[method]).toHaveBeenCalled());
|
||||
}
|
||||
|
||||
if (data.not_called) {
|
||||
$.each(data.not_called, (method, obj) => expect(obj[method]).not.toHaveBeenCalled());
|
||||
}
|
||||
|
||||
if (data.isPropagationStopped) {
|
||||
expect(e.isPropagationStopped()).toBeTruthy();
|
||||
} else {
|
||||
expect(e.isPropagationStopped()).toBeFalsy();
|
||||
}
|
||||
|
||||
expect(value).toBe(data.returnedValue);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
describe('calculate', function() {
|
||||
beforeEach(function() {
|
||||
$('#calculator_input').val('1+2');
|
||||
spyOn($, 'getWithPrefix').and.callFake((url, data, callback) => callback({ result: 3 }));
|
||||
this.calculator.calculate();
|
||||
});
|
||||
|
||||
it('send data to /calculate', () =>
|
||||
expect($.getWithPrefix).toHaveBeenCalledWith('/calculate',
|
||||
{equation: '1+2'}
|
||||
, jasmine.any(Function))
|
||||
);
|
||||
|
||||
it('update the calculator output', () => expect($('#calculator_output').val()).toEqual('3'));
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
describe 'Courseware', ->
|
||||
describe 'start', ->
|
||||
it 'binds the Logger', ->
|
||||
spyOn(Logger, 'bind')
|
||||
Courseware.start()
|
||||
expect(Logger.bind).toHaveBeenCalled()
|
||||
|
||||
describe 'render', ->
|
||||
beforeEach ->
|
||||
jasmine.stubRequests()
|
||||
@courseware = new Courseware
|
||||
spyOn(window, 'Histogram')
|
||||
spyOn(window, 'Problem')
|
||||
spyOn(window, 'Video')
|
||||
spyOn(XBlock, 'initializeBlocks')
|
||||
setFixtures """
|
||||
<div class="course-content">
|
||||
<div id="video_1" class="video" data-streams="1.0:abc1234"></div>
|
||||
<div id="video_2" class="video" data-streams="1.0:def5678"></div>
|
||||
<div id="problem_3" class="problems-wrapper" data-problem-id="3" data-url="/example/url/">
|
||||
<div id="histogram_3" class="histogram" data-histogram="[[0, 1]]" style="height: 20px; display: block;">
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
@courseware.render()
|
||||
|
||||
it 'ensure that the XModules have been loaded', ->
|
||||
expect(XBlock.initializeBlocks).toHaveBeenCalled()
|
||||
|
||||
it 'detect the histrogram element and convert it', ->
|
||||
expect(window.Histogram).toHaveBeenCalledWith('3', [[0, 1]])
|
||||
35
lms/static/coffee/spec/courseware_spec.js
Normal file
35
lms/static/coffee/spec/courseware_spec.js
Normal file
@@ -0,0 +1,35 @@
|
||||
describe('Courseware', function() {
|
||||
describe('start', () =>
|
||||
it('binds the Logger', function() {
|
||||
spyOn(Logger, 'bind');
|
||||
Courseware.start();
|
||||
expect(Logger.bind).toHaveBeenCalled();
|
||||
})
|
||||
);
|
||||
|
||||
describe('render', function() {
|
||||
beforeEach(function() {
|
||||
jasmine.stubRequests();
|
||||
this.courseware = new Courseware;
|
||||
spyOn(window, 'Histogram');
|
||||
spyOn(window, 'Problem');
|
||||
spyOn(window, 'Video');
|
||||
spyOn(XBlock, 'initializeBlocks');
|
||||
setFixtures(`\
|
||||
<div class="course-content">
|
||||
<div id="video_1" class="video" data-streams="1.0:abc1234"></div>
|
||||
<div id="video_2" class="video" data-streams="1.0:def5678"></div>
|
||||
<div id="problem_3" class="problems-wrapper" data-problem-id="3" data-url="/example/url/">
|
||||
<div id="histogram_3" class="histogram" data-histogram="[[0, 1]]" style="height: 20px; display: block;">
|
||||
</div>
|
||||
</div>\
|
||||
`
|
||||
);
|
||||
this.courseware.render();
|
||||
});
|
||||
|
||||
it('ensure that the XModules have been loaded', () => expect(XBlock.initializeBlocks).toHaveBeenCalled());
|
||||
|
||||
it('detect the histrogram element and convert it', () => expect(window.Histogram).toHaveBeenCalledWith('3', [[0, 1]]));
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
describe 'FeedbackForm', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'coffee/fixtures/feedback_form.html'
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
new FeedbackForm
|
||||
spyOn($, 'postWithPrefix').and.callFake (url, data, callback, format) ->
|
||||
callback()
|
||||
|
||||
it 'post data to /send_feedback on click', ->
|
||||
$('#feedback_subject').val 'Awesome!'
|
||||
$('#feedback_message').val 'This site is really good.'
|
||||
$('#feedback_button').click()
|
||||
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/send_feedback', {
|
||||
subject: 'Awesome!'
|
||||
message: 'This site is really good.'
|
||||
url: window.location.href
|
||||
}, jasmine.any(Function), 'json'
|
||||
|
||||
it 'replace the form with a thank you message', ->
|
||||
$('#feedback_button').click()
|
||||
|
||||
expect($('#feedback_div').html()).toEqual 'Feedback submitted. Thank you'
|
||||
28
lms/static/coffee/spec/feedback_form_spec.js
Normal file
28
lms/static/coffee/spec/feedback_form_spec.js
Normal file
@@ -0,0 +1,28 @@
|
||||
describe('FeedbackForm', function() {
|
||||
beforeEach(() => loadFixtures('coffee/fixtures/feedback_form.html'));
|
||||
|
||||
describe('constructor', function() {
|
||||
beforeEach(function() {
|
||||
new FeedbackForm;
|
||||
spyOn($, 'postWithPrefix').and.callFake((url, data, callback, format) => callback());
|
||||
});
|
||||
|
||||
it('post data to /send_feedback on click', function() {
|
||||
$('#feedback_subject').val('Awesome!');
|
||||
$('#feedback_message').val('This site is really good.');
|
||||
$('#feedback_button').click();
|
||||
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith('/send_feedback', {
|
||||
subject: 'Awesome!',
|
||||
message: 'This site is really good.',
|
||||
url: window.location.href
|
||||
}, jasmine.any(Function), 'json');
|
||||
});
|
||||
|
||||
it('replace the form with a thank you message', function() {
|
||||
$('#feedback_button').click();
|
||||
|
||||
expect($('#feedback_div').html()).toEqual('Feedback submitted. Thank you');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
jasmine.stubbedMetadata =
|
||||
slowerSpeedYoutubeId:
|
||||
id: 'slowerSpeedYoutubeId'
|
||||
duration: 300
|
||||
normalSpeedYoutubeId:
|
||||
id: 'normalSpeedYoutubeId'
|
||||
duration: 200
|
||||
bogus:
|
||||
duration: 100
|
||||
|
||||
jasmine.stubbedCaption =
|
||||
start: [0, 10000, 20000, 30000]
|
||||
text: ['Caption at 0', 'Caption at 10000', 'Caption at 20000', 'Caption at 30000']
|
||||
|
||||
jasmine.stubRequests = ->
|
||||
spyOn($, 'ajax').and.callFake (settings) ->
|
||||
if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/
|
||||
settings.success data: jasmine.stubbedMetadata[match[1]]
|
||||
else if match = settings.url.match /static\/subs\/(.+)\.srt\.sjson/
|
||||
settings.success jasmine.stubbedCaption
|
||||
else if settings.url.match /modx\/.+\/problem_get$/
|
||||
settings.success html: readFixtures('problem_content.html')
|
||||
else if settings.url == '/calculate' ||
|
||||
settings.url.match(/modx\/.+\/goto_position$/) ||
|
||||
settings.url.match(/event$/) ||
|
||||
settings.url.match(/modx\/.+\/problem_(check|reset|show|save)$/)
|
||||
# do nothing
|
||||
else
|
||||
throw "External request attempted for #{settings.url}, which is not defined."
|
||||
|
||||
jasmine.stubYoutubePlayer = ->
|
||||
YT.Player = -> jasmine.createSpyObj 'YT.Player', ['cueVideoById', 'getVideoEmbedCode',
|
||||
'getCurrentTime', 'getPlayerState', 'getVolume', 'setVolume', 'loadVideoById',
|
||||
'playVideo', 'pauseVideo', 'seekTo']
|
||||
|
||||
jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) ->
|
||||
enableParts = [enableParts] unless $.isArray(enableParts)
|
||||
|
||||
suite = context.suite
|
||||
currentPartName = suite.description while suite = suite.parentSuite
|
||||
enableParts.push currentPartName
|
||||
|
||||
for part in ['VideoCaption', 'VideoSpeedControl', 'VideoVolumeControl', 'VideoProgressSlider']
|
||||
unless $.inArray(part, enableParts) >= 0
|
||||
spyOn window, part
|
||||
|
||||
loadFixtures 'video.html'
|
||||
jasmine.stubRequests()
|
||||
YT.Player = undefined
|
||||
context.video = new Video 'example', '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
|
||||
jasmine.stubYoutubePlayer()
|
||||
if createPlayer
|
||||
return new VideoPlayer(video: context.video)
|
||||
|
||||
# Stub Youtube API
|
||||
window.YT =
|
||||
PlayerState:
|
||||
UNSTARTED: -1
|
||||
ENDED: 0
|
||||
PLAYING: 1
|
||||
PAUSED: 2
|
||||
BUFFERING: 3
|
||||
CUED: 5
|
||||
|
||||
# Stub jQuery.cookie
|
||||
$.cookie = jasmine.createSpy('jQuery.cookie').and.returnValue '1.0'
|
||||
|
||||
# Stub jQuery.qtip
|
||||
$.fn.qtip = jasmine.createSpy 'jQuery.qtip'
|
||||
|
||||
# Stub jQuery.scrollTo
|
||||
$.fn.scrollTo = jasmine.createSpy 'jQuery.scrollTo'
|
||||
95
lms/static/coffee/spec/helper.js
Normal file
95
lms/static/coffee/spec/helper.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
jasmine.stubbedMetadata = {
|
||||
slowerSpeedYoutubeId: {
|
||||
id: 'slowerSpeedYoutubeId',
|
||||
duration: 300
|
||||
},
|
||||
normalSpeedYoutubeId: {
|
||||
id: 'normalSpeedYoutubeId',
|
||||
duration: 200
|
||||
},
|
||||
bogus: {
|
||||
duration: 100
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.stubbedCaption = {
|
||||
start: [0, 10000, 20000, 30000],
|
||||
text: ['Caption at 0', 'Caption at 10000', 'Caption at 20000', 'Caption at 30000']
|
||||
};
|
||||
|
||||
jasmine.stubRequests = () =>
|
||||
spyOn($, 'ajax').and.callFake(function(settings) {
|
||||
let match;
|
||||
if (match = settings.url.match(/youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/)) {
|
||||
return settings.success({data: jasmine.stubbedMetadata[match[1]]});
|
||||
} else if (match = settings.url.match(/static\/subs\/(.+)\.srt\.sjson/)) {
|
||||
return settings.success(jasmine.stubbedCaption);
|
||||
} else if (settings.url.match(/modx\/.+\/problem_get$/)) {
|
||||
return settings.success({html: readFixtures('problem_content.html')});
|
||||
} else if ((settings.url === '/calculate') ||
|
||||
settings.url.match(/modx\/.+\/goto_position$/) ||
|
||||
settings.url.match(/event$/) ||
|
||||
settings.url.match(/modx\/.+\/problem_(check|reset|show|save)$/)) {
|
||||
// do nothing
|
||||
} else {
|
||||
throw `External request attempted for ${settings.url}, which is not defined.`;
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
jasmine.stubYoutubePlayer = () =>
|
||||
YT.Player = () => jasmine.createSpyObj('YT.Player', ['cueVideoById', 'getVideoEmbedCode',
|
||||
'getCurrentTime', 'getPlayerState', 'getVolume', 'setVolume', 'loadVideoById',
|
||||
'playVideo', 'pauseVideo', 'seekTo'])
|
||||
;
|
||||
|
||||
jasmine.stubVideoPlayer = function(context, enableParts, createPlayer) {
|
||||
let currentPartName;
|
||||
if (createPlayer == null) { createPlayer = true; }
|
||||
if (!$.isArray(enableParts)) { enableParts = [enableParts]; }
|
||||
|
||||
let { suite } = context;
|
||||
while ((suite = suite.parentSuite)) { currentPartName = suite.description; }
|
||||
enableParts.push(currentPartName);
|
||||
|
||||
for (let part of ['VideoCaption', 'VideoSpeedControl', 'VideoVolumeControl', 'VideoProgressSlider']) {
|
||||
if (!($.inArray(part, enableParts) >= 0)) {
|
||||
spyOn(window, part);
|
||||
}
|
||||
}
|
||||
|
||||
loadFixtures('video.html');
|
||||
jasmine.stubRequests();
|
||||
YT.Player = undefined;
|
||||
context.video = new Video('example', '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId');
|
||||
jasmine.stubYoutubePlayer();
|
||||
if (createPlayer) {
|
||||
return new VideoPlayer({video: context.video});
|
||||
}
|
||||
};
|
||||
|
||||
// Stub Youtube API
|
||||
window.YT = {
|
||||
PlayerState: {
|
||||
UNSTARTED: -1,
|
||||
ENDED: 0,
|
||||
PLAYING: 1,
|
||||
PAUSED: 2,
|
||||
BUFFERING: 3,
|
||||
CUED: 5
|
||||
}
|
||||
};
|
||||
|
||||
// Stub jQuery.cookie
|
||||
$.cookie = jasmine.createSpy('jQuery.cookie').and.returnValue('1.0');
|
||||
|
||||
// Stub jQuery.qtip
|
||||
$.fn.qtip = jasmine.createSpy('jQuery.qtip');
|
||||
|
||||
// Stub jQuery.scrollTo
|
||||
$.fn.scrollTo = jasmine.createSpy('jQuery.scrollTo');
|
||||
@@ -1,54 +0,0 @@
|
||||
describe 'Histogram', ->
|
||||
beforeEach ->
|
||||
spyOn $, 'plot'
|
||||
|
||||
describe 'constructor', ->
|
||||
it 'instantiate the data arrays', ->
|
||||
histogram = new Histogram 1, []
|
||||
expect(histogram.xTicks).toEqual []
|
||||
expect(histogram.yTicks).toEqual []
|
||||
expect(histogram.data).toEqual []
|
||||
|
||||
describe 'calculate', ->
|
||||
beforeEach ->
|
||||
@histogram = new Histogram(1, [[null, 1], [1, 1], [2, 2], [3, 3]])
|
||||
|
||||
it 'store the correct value for data', ->
|
||||
expect(@histogram.data).toEqual [[1, Math.log(2)], [2, Math.log(3)], [3, Math.log(4)]]
|
||||
|
||||
it 'store the correct value for x ticks', ->
|
||||
expect(@histogram.xTicks).toEqual [[1, '1'], [2, '2'], [3, '3']]
|
||||
|
||||
it 'store the correct value for y ticks', ->
|
||||
expect(@histogram.yTicks).toEqual
|
||||
|
||||
describe 'render', ->
|
||||
it 'call flot with correct option', ->
|
||||
new Histogram(1, [[1, 1], [2, 2], [3, 3]])
|
||||
|
||||
firstArg = $.plot.calls.mostRecent().args[0]
|
||||
secondArg = $.plot.calls.mostRecent().args[1]
|
||||
thirdArg = $.plot.calls.mostRecent().args[2]
|
||||
|
||||
expect(firstArg.selector).toEqual($("#histogram_1").selector)
|
||||
expect(secondArg).toEqual([
|
||||
data: [[1, Math.log(2)], [2, Math.log(3)], [3, Math.log(4)]]
|
||||
bars:
|
||||
show: true
|
||||
align: 'center'
|
||||
lineWidth: 0
|
||||
fill: 1.0
|
||||
color: "#b72121"
|
||||
])
|
||||
expect(thirdArg).toEqual(
|
||||
xaxis:
|
||||
min: -1
|
||||
max: 4
|
||||
ticks: [[1, '1'], [2, '2'], [3, '3']]
|
||||
tickLength: 0
|
||||
yaxis:
|
||||
min: 0.0
|
||||
max: Math.log(4) * 1.1
|
||||
ticks: [[Math.log(2), '1'], [Math.log(3), '2'], [Math.log(4), '3']]
|
||||
labelWidth: 50
|
||||
)
|
||||
67
lms/static/coffee/spec/histogram_spec.js
Normal file
67
lms/static/coffee/spec/histogram_spec.js
Normal file
@@ -0,0 +1,67 @@
|
||||
describe('Histogram', function() {
|
||||
beforeEach(() => spyOn($, 'plot'));
|
||||
|
||||
describe('constructor', () =>
|
||||
it('instantiate the data arrays', function() {
|
||||
const histogram = new Histogram(1, []);
|
||||
expect(histogram.xTicks).toEqual([]);
|
||||
expect(histogram.yTicks).toEqual([]);
|
||||
expect(histogram.data).toEqual([]);
|
||||
})
|
||||
);
|
||||
|
||||
describe('calculate', function() {
|
||||
beforeEach(function() {
|
||||
this.histogram = new Histogram(1, [[null, 1], [1, 1], [2, 2], [3, 3]]);
|
||||
});
|
||||
|
||||
it('store the correct value for data', function() {
|
||||
expect(this.histogram.data).toEqual([[1, Math.log(2)], [2, Math.log(3)], [3, Math.log(4)]]);
|
||||
});
|
||||
|
||||
it('store the correct value for x ticks', function() {
|
||||
expect(this.histogram.xTicks).toEqual([[1, '1'], [2, '2'], [3, '3']]);
|
||||
});
|
||||
|
||||
it('store the correct value for y ticks', function() {
|
||||
expect(this.histogram.yTicks).toEqual;
|
||||
});
|
||||
});
|
||||
|
||||
describe('render', () =>
|
||||
it('call flot with correct option', function() {
|
||||
new Histogram(1, [[1, 1], [2, 2], [3, 3]]);
|
||||
|
||||
const firstArg = $.plot.calls.mostRecent().args[0];
|
||||
const secondArg = $.plot.calls.mostRecent().args[1];
|
||||
const thirdArg = $.plot.calls.mostRecent().args[2];
|
||||
|
||||
expect(firstArg.selector).toEqual($("#histogram_1").selector);
|
||||
expect(secondArg).toEqual([{
|
||||
data: [[1, Math.log(2)], [2, Math.log(3)], [3, Math.log(4)]],
|
||||
bars: {
|
||||
show: true,
|
||||
align: 'center',
|
||||
lineWidth: 0,
|
||||
fill: 1.0
|
||||
},
|
||||
color: "#b72121"
|
||||
}
|
||||
]);
|
||||
expect(thirdArg).toEqual({
|
||||
xaxis: {
|
||||
min: -1,
|
||||
max: 4,
|
||||
ticks: [[1, '1'], [2, '2'], [3, '3']],
|
||||
tickLength: 0
|
||||
},
|
||||
yaxis: {
|
||||
min: 0.0,
|
||||
max: Math.log(4) * 1.1,
|
||||
ticks: [[Math.log(2), '1'], [Math.log(3), '2'], [Math.log(4), '3']],
|
||||
labelWidth: 50
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
describe 'Tab', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'coffee/fixtures/tab.html'
|
||||
@items = $.parseJSON readFixtures('coffee/fixtures/items.json')
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
spyOn($.fn, 'tabs')
|
||||
@tab = new Tab 1, @items
|
||||
|
||||
it 'set the element', ->
|
||||
expect(@tab.el).toEqual $('#tab_1')
|
||||
|
||||
it 'build the tabs', ->
|
||||
links = $('.navigation li>a').map(-> $(this).attr('href')).get()
|
||||
expect(links).toEqual ['#tab-1-0', '#tab-1-1', '#tab-1-2']
|
||||
|
||||
it 'build the container', ->
|
||||
containers = $('section').map(-> $(this).attr('id')).get()
|
||||
expect(containers).toEqual ['tab-1-0', 'tab-1-1', 'tab-1-2']
|
||||
|
||||
it 'bind the tabs', ->
|
||||
expect($.fn.tabs).toHaveBeenCalledWith show: @tab.onShow
|
||||
|
||||
# As of jQuery 1.9, the onShow callback is deprecated
|
||||
# http://jqueryui.com/upgrade-guide/1.9/#deprecated-show-event-renamed-to-activate
|
||||
# The code below tests that onShow does what is expected,
|
||||
# but note that onShow will NOT be called when the user
|
||||
# clicks on the tab if we're using jQuery version >= 1.9
|
||||
describe 'onShow', ->
|
||||
beforeEach ->
|
||||
@tab = new Tab 1, @items
|
||||
@tab.onShow($('#tab-1-0'), {'index': 1})
|
||||
|
||||
it 'replace content in the container', ->
|
||||
@tab.onShow($('#tab-1-1'), {'index': 1})
|
||||
expect($('#tab-1-0').html()).toEqual ''
|
||||
expect($('#tab-1-1').html()).toEqual 'Video 2'
|
||||
expect($('#tab-1-2').html()).toEqual ''
|
||||
|
||||
it 'trigger contentChanged event on the element', ->
|
||||
spyOnEvent @tab.el, 'contentChanged'
|
||||
@tab.onShow($('#tab-1-1'), {'index': 1})
|
||||
expect('contentChanged').toHaveBeenTriggeredOn @tab.el
|
||||
56
lms/static/coffee/spec/modules/tab_spec.js
Normal file
56
lms/static/coffee/spec/modules/tab_spec.js
Normal file
@@ -0,0 +1,56 @@
|
||||
describe('Tab', function() {
|
||||
beforeEach(function() {
|
||||
loadFixtures('coffee/fixtures/tab.html');
|
||||
this.items = $.parseJSON(readFixtures('coffee/fixtures/items.json'));
|
||||
});
|
||||
|
||||
describe('constructor', function() {
|
||||
beforeEach(function() {
|
||||
spyOn($.fn, 'tabs');
|
||||
this.tab = new Tab(1, this.items);
|
||||
});
|
||||
|
||||
it('set the element', function() {
|
||||
expect(this.tab.el).toEqual($('#tab_1'));
|
||||
});
|
||||
|
||||
it('build the tabs', function() {
|
||||
const links = $('.navigation li>a').map(function() { return $(this).attr('href'); }).get();
|
||||
expect(links).toEqual(['#tab-1-0', '#tab-1-1', '#tab-1-2']);
|
||||
});
|
||||
|
||||
it('build the container', function() {
|
||||
const containers = $('section').map(function() { return $(this).attr('id'); }).get();
|
||||
expect(containers).toEqual(['tab-1-0', 'tab-1-1', 'tab-1-2']);
|
||||
});
|
||||
|
||||
it('bind the tabs', function() {
|
||||
expect($.fn.tabs).toHaveBeenCalledWith({show: this.tab.onShow});
|
||||
});
|
||||
});
|
||||
|
||||
// As of jQuery 1.9, the onShow callback is deprecated
|
||||
// http://jqueryui.com/upgrade-guide/1.9/#deprecated-show-event-renamed-to-activate
|
||||
// The code below tests that onShow does what is expected,
|
||||
// but note that onShow will NOT be called when the user
|
||||
// clicks on the tab if we're using jQuery version >= 1.9
|
||||
describe('onShow', function() {
|
||||
beforeEach(function() {
|
||||
this.tab = new Tab(1, this.items);
|
||||
this.tab.onShow($('#tab-1-0'), {'index': 1});
|
||||
});
|
||||
|
||||
it('replace content in the container', function() {
|
||||
this.tab.onShow($('#tab-1-1'), {'index': 1});
|
||||
expect($('#tab-1-0').html()).toEqual('');
|
||||
expect($('#tab-1-1').html()).toEqual('Video 2');
|
||||
expect($('#tab-1-2').html()).toEqual('');
|
||||
});
|
||||
|
||||
it('trigger contentChanged event on the element', function() {
|
||||
spyOnEvent(this.tab.el, 'contentChanged');
|
||||
this.tab.onShow($('#tab-1-1'), {'index': 1});
|
||||
expect('contentChanged').toHaveBeenTriggeredOn(this.tab.el);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
describe "RequireJS namespacing", ->
|
||||
beforeEach ->
|
||||
|
||||
# Jasmine does not provide a way to use the typeof operator. We need
|
||||
# to create our own custom matchers so that a TypeError is not thrown.
|
||||
jasmine.addMatchers
|
||||
requirejsTobeUndefined: ->
|
||||
{
|
||||
compare: ->
|
||||
{
|
||||
pass: typeof requirejs is "undefined"
|
||||
}
|
||||
}
|
||||
|
||||
requireTobeUndefined: ->
|
||||
{
|
||||
compare: ->
|
||||
{
|
||||
pass: typeof require is "undefined"
|
||||
}
|
||||
}
|
||||
|
||||
defineTobeUndefined: ->
|
||||
{
|
||||
compare: ->
|
||||
{
|
||||
pass: typeof define is "undefined"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
it "check that the RequireJS object is present in the global namespace", ->
|
||||
expect(RequireJS).toEqual jasmine.any(Object)
|
||||
expect(window.RequireJS).toEqual jasmine.any(Object)
|
||||
|
||||
it "check that requirejs(), require(), and define() are not in the global namespace", ->
|
||||
|
||||
# The custom matchers that we defined in the beforeEach() function do
|
||||
# not operate on an object. We pass a dummy empty object {} not to
|
||||
# confuse Jasmine.
|
||||
expect({}).requirejsTobeUndefined()
|
||||
expect({}).requireTobeUndefined()
|
||||
expect({}).defineTobeUndefined()
|
||||
expect(window.requirejs).not.toBeDefined()
|
||||
expect(window.require).not.toBeDefined()
|
||||
expect(window.define).not.toBeDefined()
|
||||
|
||||
|
||||
describe "RequireJS module creation", ->
|
||||
inDefineCallback = undefined
|
||||
inRequireCallback = undefined
|
||||
it "check that we can use RequireJS to define() and require() a module", (done) ->
|
||||
d1 = $.Deferred()
|
||||
d2 = $.Deferred()
|
||||
# Because Require JS works asynchronously when defining and requiring
|
||||
# modules, we need to use the special Jasmine functions runs(), and
|
||||
# waitsFor() to set up this test.
|
||||
func = () ->
|
||||
|
||||
# Initialize the variable that we will test for. They will be set
|
||||
# to true in the appropriate callback functions called by Require
|
||||
# JS. If their values do not change, this will mean that something
|
||||
# is not working as is intended.
|
||||
inDefineCallback = false
|
||||
inRequireCallback = false
|
||||
|
||||
# Define our test module.
|
||||
RequireJS.define "test_module", [], ->
|
||||
inDefineCallback = true
|
||||
|
||||
d1.resolve()
|
||||
|
||||
# This module returns an object. It can be accessed via the
|
||||
# Require JS require() function.
|
||||
module_status: "OK"
|
||||
|
||||
|
||||
# Require our defined test module.
|
||||
RequireJS.require ["test_module"], (test_module) ->
|
||||
inRequireCallback = true
|
||||
|
||||
# If our test module was defined properly, then we should
|
||||
# be able to get the object it returned, and query some
|
||||
# property.
|
||||
expect(test_module.module_status).toBe "OK"
|
||||
|
||||
d2.resolve()
|
||||
|
||||
func()
|
||||
# We will wait before checking if our module was defined and that we were able to require() the module.
|
||||
$.when(d1, d2).done(->
|
||||
# The final test behavior
|
||||
expect(inDefineCallback).toBeTruthy()
|
||||
expect(inRequireCallback).toBeTruthy()
|
||||
).always(done)
|
||||
114
lms/static/coffee/spec/requirejs_spec.js
Normal file
114
lms/static/coffee/spec/requirejs_spec.js
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
describe("RequireJS namespacing", function() {
|
||||
beforeEach(() =>
|
||||
|
||||
// Jasmine does not provide a way to use the typeof operator. We need
|
||||
// to create our own custom matchers so that a TypeError is not thrown.
|
||||
jasmine.addMatchers({
|
||||
requirejsTobeUndefined() {
|
||||
return {
|
||||
compare() {
|
||||
return {
|
||||
pass: typeof requirejs === "undefined"
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
requireTobeUndefined() {
|
||||
return {
|
||||
compare() {
|
||||
return {
|
||||
pass: typeof require === "undefined"
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
defineTobeUndefined() {
|
||||
return {
|
||||
compare() {
|
||||
return {
|
||||
pass: typeof define === "undefined"
|
||||
};
|
||||
}
|
||||
};
|
||||
}}));
|
||||
|
||||
|
||||
it("check that the RequireJS object is present in the global namespace", function() {
|
||||
expect(RequireJS).toEqual(jasmine.any(Object));
|
||||
expect(window.RequireJS).toEqual(jasmine.any(Object));
|
||||
});
|
||||
|
||||
it("check that requirejs(), require(), and define() are not in the global namespace", function() {
|
||||
|
||||
// The custom matchers that we defined in the beforeEach() function do
|
||||
// not operate on an object. We pass a dummy empty object {} not to
|
||||
// confuse Jasmine.
|
||||
expect({}).requirejsTobeUndefined();
|
||||
expect({}).requireTobeUndefined();
|
||||
expect({}).defineTobeUndefined();
|
||||
expect(window.requirejs).not.toBeDefined();
|
||||
expect(window.require).not.toBeDefined();
|
||||
expect(window.define).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("RequireJS module creation", function() {
|
||||
let inDefineCallback = undefined;
|
||||
let inRequireCallback = undefined;
|
||||
it("check that we can use RequireJS to define() and require() a module", function(done) {
|
||||
const d1 = $.Deferred();
|
||||
const d2 = $.Deferred();
|
||||
// Because Require JS works asynchronously when defining and requiring
|
||||
// modules, we need to use the special Jasmine functions runs(), and
|
||||
// waitsFor() to set up this test.
|
||||
const func = function() {
|
||||
|
||||
// Initialize the variable that we will test for. They will be set
|
||||
// to true in the appropriate callback functions called by Require
|
||||
// JS. If their values do not change, this will mean that something
|
||||
// is not working as is intended.
|
||||
inDefineCallback = false;
|
||||
inRequireCallback = false;
|
||||
|
||||
// Define our test module.
|
||||
RequireJS.define("test_module", [], function() {
|
||||
inDefineCallback = true;
|
||||
|
||||
d1.resolve();
|
||||
|
||||
// This module returns an object. It can be accessed via the
|
||||
// Require JS require() function.
|
||||
return {module_status: "OK"};
|
||||
});
|
||||
|
||||
|
||||
// Require our defined test module.
|
||||
return RequireJS.require(["test_module"], function(test_module) {
|
||||
inRequireCallback = true;
|
||||
|
||||
// If our test module was defined properly, then we should
|
||||
// be able to get the object it returned, and query some
|
||||
// property.
|
||||
expect(test_module.module_status).toBe("OK");
|
||||
|
||||
return d2.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
func();
|
||||
// We will wait before checking if our module was defined and that we were able to require() the module.
|
||||
$.when(d1, d2).done(function() {
|
||||
// The final test behavior
|
||||
expect(inDefineCallback).toBeTruthy();
|
||||
expect(inRequireCallback).toBeTruthy();
|
||||
}).always(done);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user