Merge pull request #15846 from edx/ormsbee/decaffeinate_tests_2

WIP: Decaffeinate Test Files (Part 2)
This commit is contained in:
Calen Pennington
2017-11-28 13:34:26 -05:00
committed by GitHub
65 changed files with 6951 additions and 6015 deletions

View File

@@ -1 +0,0 @@
*.js

View File

@@ -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")

View 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");
});
})
)
);

View File

@@ -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)

View 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);
});
})
);

View File

@@ -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()

View 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();
});
});
})
);

View File

@@ -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.')

View 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.');
});
})
)
);

View File

@@ -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)

View 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);
});
});
})
);

View File

@@ -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)

View 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);
});
});
});

View File

@@ -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'
)

View 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'
});
});
});
})
);

View File

@@ -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")

View 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");
});
});
});
});

View File

@@ -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)

View 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);
});
});
})
);

View File

@@ -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')

View 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');
});
});
});
});

View File

@@ -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")

View 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");
});
});
});

View File

@@ -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();

View 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();
});
});
})
);

View File

@@ -1,4 +0,0 @@
test_problem_display.js
test_problem_generator.js
test_problem_grader.js
xproblem.js

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View 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;

View File

@@ -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

View File

@@ -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)

View File

@@ -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));
});
});

View File

@@ -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()

View 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();
});
});
});

View File

@@ -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')

View 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');
});
});
});

View File

@@ -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>
""")

View 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>\
`);
});
});
});

View File

@@ -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>
""")

View 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>\
`);
});
});

View File

@@ -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)

View 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);
});
});
});

View File

@@ -1 +0,0 @@
*.js

View File

@@ -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)

View File

@@ -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);
});
});

View File

@@ -1 +0,0 @@
*.js

View File

@@ -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')

View 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'));
});
});

View File

@@ -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]])

View 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]]));
});
});

View File

@@ -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'

View 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');
});
});
});

View File

@@ -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'

View 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');

View File

@@ -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
)

View 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
}
});
})
);
});

View File

@@ -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

View 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);
});
});
});

View File

@@ -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)

View 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);
});
});