diff --git a/cms/static/coffee/.gitignore b/cms/static/coffee/.gitignore deleted file mode 100644 index a6c7c2852d..0000000000 --- a/cms/static/coffee/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.js diff --git a/cms/static/coffee/spec/models/course_spec.coffee b/cms/static/coffee/spec/models/course_spec.coffee deleted file mode 100644 index b6dc658e5a..0000000000 --- a/cms/static/coffee/spec/models/course_spec.coffee +++ /dev/null @@ -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") diff --git a/cms/static/coffee/spec/models/course_spec.js b/cms/static/coffee/spec/models/course_spec.js new file mode 100644 index 0000000000..ab94fa9381 --- /dev/null +++ b/cms/static/coffee/spec/models/course_spec.js @@ -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"); + }); + }) + ) +); diff --git a/cms/static/coffee/spec/models/metadata_spec.coffee b/cms/static/coffee/spec/models/metadata_spec.coffee deleted file mode 100644 index ba3c4a2b65..0000000000 --- a/cms/static/coffee/spec/models/metadata_spec.coffee +++ /dev/null @@ -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) - diff --git a/cms/static/coffee/spec/models/metadata_spec.js b/cms/static/coffee/spec/models/metadata_spec.js new file mode 100644 index 0000000000..a90c1f2ad6 --- /dev/null +++ b/cms/static/coffee/spec/models/metadata_spec.js @@ -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); + }); + }) +); + diff --git a/cms/static/coffee/spec/models/section_spec.coffee b/cms/static/coffee/spec/models/section_spec.coffee deleted file mode 100644 index 8a954b50dc..0000000000 --- a/cms/static/coffee/spec/models/section_spec.coffee +++ /dev/null @@ -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() diff --git a/cms/static/coffee/spec/models/section_spec.js b/cms/static/coffee/spec/models/section_spec.js new file mode 100644 index 0000000000..c0c89c2d15 --- /dev/null +++ b/cms/static/coffee/spec/models/section_spec.js @@ -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(); + }); + }); + }) +); diff --git a/cms/static/coffee/spec/models/settings_course_grader_spec.coffee b/cms/static/coffee/spec/models/settings_course_grader_spec.coffee deleted file mode 100644 index 23cf22d371..0000000000 --- a/cms/static/coffee/spec/models/settings_course_grader_spec.coffee +++ /dev/null @@ -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.') - diff --git a/cms/static/coffee/spec/models/settings_course_grader_spec.js b/cms/static/coffee/spec/models/settings_course_grader_spec.js new file mode 100644 index 0000000000..b5d856fdda --- /dev/null +++ b/cms/static/coffee/spec/models/settings_course_grader_spec.js @@ -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.'); + }); + }) + ) +); + diff --git a/cms/static/coffee/spec/models/settings_grading_spec.coffee b/cms/static/coffee/spec/models/settings_grading_spec.coffee deleted file mode 100644 index 4092f4f161..0000000000 --- a/cms/static/coffee/spec/models/settings_grading_spec.coffee +++ /dev/null @@ -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) diff --git a/cms/static/coffee/spec/models/settings_grading_spec.js b/cms/static/coffee/spec/models/settings_grading_spec.js new file mode 100644 index 0000000000..f74e5b0797 --- /dev/null +++ b/cms/static/coffee/spec/models/settings_grading_spec.js @@ -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); + }); + }); + }) +); diff --git a/cms/static/coffee/spec/models/textbook_spec.coffee b/cms/static/coffee/spec/models/textbook_spec.coffee deleted file mode 100644 index dc10df8a47..0000000000 --- a/cms/static/coffee/spec/models/textbook_spec.coffee +++ /dev/null @@ -1,198 +0,0 @@ - -define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/chapter", "js/collections/chapter", "cms/js/main"], -(Backbone, Textbook, TextbookSet, Chapter, ChapterSet, main) -> - - describe "Textbook model", -> - beforeEach -> - main() - @model = new Textbook() - CMS.URL.TEXTBOOKS = "/textbooks" - - afterEach -> - delete CMS.URL.TEXTBOOKS - - describe "Basic", -> - it "should have an empty name by default", -> - expect(@model.get("name")).toEqual("") - - it "should not show chapters by default", -> - expect(@model.get("showChapters")).toBeFalsy() - - it "should have a ChapterSet with one chapter by default", -> - chapters = @model.get("chapters") - expect(chapters).toBeInstanceOf(ChapterSet) - expect(chapters.length).toEqual(1) - expect(chapters.at(0).isEmpty()).toBeTruthy() - - it "should be empty by default", -> - expect(@model.isEmpty()).toBeTruthy() - - it "should have a URL root", -> - urlRoot = _.result(@model, 'urlRoot') - expect(urlRoot).toBeTruthy() - - it "should be able to reset itself", -> - @model.set("name", "foobar") - @model.reset() - expect(@model.get("name")).toEqual("") - - it "should not be dirty by default", -> - expect(@model.isDirty()).toBeFalsy() - - it "should be dirty after it's been changed", -> - @model.set("name", "foobar") - expect(@model.isDirty()).toBeTruthy() - - it "should not be dirty after calling setOriginalAttributes", -> - @model.set("name", "foobar") - @model.setOriginalAttributes() - expect(@model.isDirty()).toBeFalsy() - - describe "Input/Output", -> - deepAttributes = (obj) -> - if obj instanceof Backbone.Model - deepAttributes(obj.attributes) - else if obj instanceof Backbone.Collection - obj.map(deepAttributes); - else if _.isArray(obj) - _.map(obj, deepAttributes); - else if _.isObject(obj) - attributes = {}; - for own prop, val of obj - attributes[prop] = deepAttributes(val) - attributes - else - obj - - it "should match server model to client model", -> - serverModelSpec = { - "tab_title": "My Textbook", - "chapters": [ - {"title": "Chapter 1", "url": "/ch1.pdf"}, - {"title": "Chapter 2", "url": "/ch2.pdf"}, - ] - } - clientModelSpec = { - "name": "My Textbook", - "showChapters": false, - "editing": false, - "chapters": [{ - "name": "Chapter 1", - "asset_path": "/ch1.pdf", - "order": 1 - }, { - "name": "Chapter 2", - "asset_path": "/ch2.pdf", - "order": 2 - } - ] - } - - model = new Textbook(serverModelSpec, {parse: true}) - expect(deepAttributes(model)).toEqual(clientModelSpec) - expect(model.toJSON()).toEqual(serverModelSpec) - - describe "Validation", -> - it "requires a name", -> - model = new Textbook({name: ""}) - expect(model.isValid()).toBeFalsy() - - it "requires at least one chapter", -> - model = new Textbook({name: "foo"}) - model.get("chapters").reset() - expect(model.isValid()).toBeFalsy() - - it "requires a valid chapter", -> - chapter = new Chapter() - chapter.isValid = -> false - model = new Textbook({name: "foo"}) - model.get("chapters").reset([chapter]) - expect(model.isValid()).toBeFalsy() - - it "requires all chapters to be valid", -> - chapter1 = new Chapter() - chapter1.isValid = -> true - chapter2 = new Chapter() - chapter2.isValid = -> false - model = new Textbook({name: "foo"}) - model.get("chapters").reset([chapter1, chapter2]) - expect(model.isValid()).toBeFalsy() - - it "can pass validation", -> - chapter = new Chapter() - chapter.isValid = -> true - model = new Textbook({name: "foo"}) - model.get("chapters").reset([chapter]) - expect(model.isValid()).toBeTruthy() - - - describe "Textbook collection", -> - beforeEach -> - CMS.URL.TEXTBOOKS = "/textbooks" - @collection = new TextbookSet() - - afterEach -> - delete CMS.URL.TEXTBOOKS - - it "should have a url set", -> - url = _.result(@collection, 'url') - expect(url).toEqual("/textbooks") - - - describe "Chapter model", -> - beforeEach -> - @model = new Chapter() - - describe "Basic", -> - it "should have a name by default", -> - expect(@model.get("name")).toEqual("") - - it "should have an asset_path by default", -> - expect(@model.get("asset_path")).toEqual("") - - it "should have an order by default", -> - expect(@model.get("order")).toEqual(1) - - it "should be empty by default", -> - expect(@model.isEmpty()).toBeTruthy() - - describe "Validation", -> - it "requires a name", -> - model = new Chapter({name: "", asset_path: "a.pdf"}) - expect(model.isValid()).toBeFalsy() - - it "requires an asset_path", -> - model = new Chapter({name: "a", asset_path: ""}) - expect(model.isValid()).toBeFalsy() - - it "can pass validation", -> - model = new Chapter({name: "a", asset_path: "a.pdf"}) - expect(model.isValid()).toBeTruthy() - - - describe "Chapter collection", -> - beforeEach -> - @collection = new ChapterSet() - - it "is empty by default", -> - expect(@collection.isEmpty()).toBeTruthy() - - it "is empty if all chapters are empty", -> - @collection.add([{}, {}, {}]) - expect(@collection.isEmpty()).toBeTruthy() - - it "is not empty if a chapter is not empty", -> - @collection.add([{}, {name: "full"}, {}]) - expect(@collection.isEmpty()).toBeFalsy() - - it "should have a nextOrder function", -> - expect(@collection.nextOrder()).toEqual(1) - @collection.add([{}]) - expect(@collection.nextOrder()).toEqual(2) - @collection.add([{}]) - expect(@collection.nextOrder()).toEqual(3) - # verify that it doesn't just return an incrementing value each time - expect(@collection.nextOrder()).toEqual(3) - # try going back one - @collection.remove(@collection.last()) - expect(@collection.nextOrder()).toEqual(2) diff --git a/cms/static/coffee/spec/models/textbook_spec.js b/cms/static/coffee/spec/models/textbook_spec.js new file mode 100644 index 0000000000..9be93eb624 --- /dev/null +++ b/cms/static/coffee/spec/models/textbook_spec.js @@ -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); + }); + }); +}); diff --git a/cms/static/coffee/spec/models/upload_spec.coffee b/cms/static/coffee/spec/models/upload_spec.coffee deleted file mode 100644 index 17cd887b36..0000000000 --- a/cms/static/coffee/spec/models/upload_spec.coffee +++ /dev/null @@ -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' - ) diff --git a/cms/static/coffee/spec/models/upload_spec.js b/cms/static/coffee/spec/models/upload_spec.js new file mode 100644 index 0000000000..f8347dc7cc --- /dev/null +++ b/cms/static/coffee/spec/models/upload_spec.js @@ -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' + }); + }); + }); + }) +); diff --git a/cms/static/coffee/spec/views/assets_spec.coffee b/cms/static/coffee/spec/views/assets_spec.coffee deleted file mode 100644 index e94d166a52..0000000000 --- a/cms/static/coffee/spec/views/assets_spec.coffee +++ /dev/null @@ -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($(" - - But in this there should be -
- Great ideas require offsetting. - - bad tests require drivel -
- - [code] - Code should be nicely monospaced. - [/code] - """) - expect(data).toXMLEqual(""" - -

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] - correct - redundant - distractor - no space - - -

Option with multiple correct ones

- - - -

Option with embedded parens

- - - -

What happens w/ empty correct options?

- - - - -
-

Explanation

-

see

-
-
-

[explanation]

-

orphaned start

-

No p tags in the below

- -

But in this there should be

-
-

Great ideas require offsetting.

-

bad tests require drivel

-
-
-          Code should be nicely monospaced.
-      
-        
-
""") - - 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(""" - - -

Multiple choice problems allow learners to select only one option. Learners can see all the options along with the problem text.

- - - Brazil timely feedback -- explain why an almost correct answer is wrong - - Germany - 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.

-
-
-
- - -

Checkbox problems allow learners to select multiple options. Learners can see all the options along with the problem text.

- - - Urdu - Finnish - Marathi - 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.

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

Multiple choice problems allow learners to select only one option. Learners can see all the options along with the problem text.

- - - - - Brazil timely feedback -- explain why an almost correct answer is wrong - - Germany - 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.

-
-
-
-
- """) - - 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(""" - - - - There are three correct choices. - - Urdu - Finnish - Marathi - French - Hungarian - - - - - - You have only choice. - - Brazil - timely feedback -- explain why an almost correct answer is wrong - - Germany - Indonesia - Russia - - - - """) - - 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(""" - - - - Choose wisely. - - Urdu - Finnish - Marathi - French - Hungarian - - - - """) - - 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(""" - - - - first second third - - Urdu - Finnish - Marathi - - - - """) - - it 'will not add empty description', -> - data = MarkdownEditingDescriptor.markdownToXml(""" - >>The following languages are in the Indo-European family:||<< - [x] Urdu - [ ] Finnish - """) - expect(data).toXMLEqual(""" - - - - - Urdu - Finnish - - - - """) diff --git a/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.js b/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.js new file mode 100644 index 0000000000..c09419a5fe --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.js @@ -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('\n

markdown

\n
'); + }); + 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('\n

foo

\n
'); + }); + // 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(` +

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:

+ + + + +

Enter the approximate value of 502*9:

+ + + + +

Enter the number of fingers on a human hand:

+ + + +

Range tolerance case

+ + + + + + +

If first and last symbols are not brackets, or they are not closed, stringresponse will appear.

+ + + + + + + +
+

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.

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

Enter 0 with a tolerance:

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

Enter 1 with a tolerance:

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

Enter 1 with a tolerance:

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

Enter 1 with a tolerance:

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

Enter 1 with a tolerance:

+ + + additional feedback + + + main feedback +
+ +
` + ); + }); + 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(` + +

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.

+ + + 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.

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

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 + 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.

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

bleh

+ + + a + b + c + + +

yatta

+ + + x + y + z + + +

testa

+ + + i + ii + iii + + + +
+

Explanation

+

When the student is ready, the explanation appears.

+
+
+
`); + }); + + 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 optioninput 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(`\ + + +

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 optioninput tag.

+

Translation between Option Response and __________ is extremely straightforward:

+ + +
+

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.

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

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?

+ + +
+

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.

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

Who lead the civil right movement in the United States of America?

+ + +
+

Explanation

+

Test Explanation.

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

Who lead the civil right movement in the United States of America?

+ + + + + +
+

Explanation

+

Test Explanation.

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

Write a number from 1 to 4.

+ + + + + +
+

Explanation

+

Test Explanation.

+
+
+
+
`); + }); + // 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(`\ + + + + + +
+

Explanation

+

Test Explanation.

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

France is a country in Europe.

+ + + + + + +

Germany is a country in Europe, too.

+ + + + + Bonn + Hamburg + Berlin + Donut + + +
`); + }); + 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(`\ + +

France is a country in Europe.

+ + + + + + +

Germany is a country in Europe, too.

+ +

What is the capital of Germany?

+ + + Bonn + Hamburg + Berlin + Donut + + +
`); + }); + + it('adds labels to formulae', function() { + const data = MarkdownEditingDescriptor.markdownToXml(`\ +>>Enter the numerical value of Pi:<< += 3.14159 +- .02\ +`); + expect(data).toXMLEqual(` + + + + + + + +`); + }); + + // 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 + + +But in this there should be +
+Great ideas require offsetting. + +bad tests require drivel +
+ +[code] +Code should be nicely monospaced. +[/code]\ +`); + expect(data).toXMLEqual(`\ + +

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] + correct + redundant + distractor + no space + + +

Option with multiple correct ones

+ + + +

Option with embedded parens

+ + + +

What happens w/ empty correct options?

+ + + + +
+

Explanation

+

see

+
+
+

[explanation]

+

orphaned start

+

No p tags in the below

+ +

But in this there should be

+
+

Great ideas require offsetting.

+

bad tests require drivel

+
+
+    Code should be nicely monospaced.
+
+  
+
`); + }); + + 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(`\ + + +

Multiple choice problems allow learners to select only one option. Learners can see all the options along with the problem text.

+ + + Brazil timely feedback -- explain why an almost correct answer is wrong + + Germany + 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.

+
+
+
+ + +

Checkbox problems allow learners to select multiple options. Learners can see all the options along with the problem text.

+ + + Urdu + Finnish + Marathi + 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.

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

Multiple choice problems allow learners to select only one option. Learners can see all the options along with the problem text.

+ + + + + Brazil timely feedback -- explain why an almost correct answer is wrong + + Germany + 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.

+
+
+
+
\ +`); + }); + + 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(`\ + + + + There are three correct choices. + + Urdu + Finnish + Marathi + French + Hungarian + + + + + + You have only choice. + + Brazil + timely feedback -- explain why an almost correct answer is wrong + + Germany + Indonesia + Russia + + +\ +`); + }); + + 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(`\ + + + + Choose wisely. + + Urdu + Finnish + Marathi + French + Hungarian + + +\ +`); + }); + + 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(`\ + + + + first second third + + Urdu + Finnish + Marathi + + +\ +`); + }); + + 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(`\ + + + + + Urdu + Finnish + + +\ +`); + }); + }); +}); diff --git a/common/lib/xmodule/xmodule/js/spec/problem/edit_spec_hint.coffee b/common/lib/xmodule/xmodule/js/spec/problem/edit_spec_hint.coffee deleted file mode 100644 index 1a9b7aad97..0000000000 --- a/common/lib/xmodule/xmodule/js/spec/problem/edit_spec_hint.coffee +++ /dev/null @@ -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(""" - -

Translation between Dropdown and ________ is straightforward.

- - - - - - - -

Clowns have funny _________ to make people laugh.

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

Translation between Dropdown and ________ is straightforward.

- - - - - -
- - - 0) zero - 1) one - 2) two - -
- """) - - 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(""" - - -

A Question ________ is answered.

- -
- - - 0) zero - 1) one - -
- """) - - it 'produces xml with fewer newlines', -> - data = MarkdownEditingDescriptor.markdownToXml(""" - >>q1<< - [[ (aa) {{ hint1 }} - bb - cc {{ hint2 }} ]] - """) - expect(data).toXMLEqual(""" - - - - - - - - - - - - - """) - - it 'produces xml even with lots of whitespace', -> - data = MarkdownEditingDescriptor.markdownToXml(""" - >>q1<< - [[ - - - aa {{ hint1 }} - - bb {{ hint2 }} - (cc) - - ]] - """) - expect(data).toXMLEqual(""" - - - - - - - - - - - - - """) - -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(""" - - - - - Apple - You're right that apple is a fruit. - Remember that apple is also a fruit. - - Mushroom - Mushroom is a fungus, not a fruit. - You're right that mushrooms aren't fruit - - Grape - You're right that grape is a fruit - Remember that grape is also a fruit. - - Mustang - Camero - I don't know what a Camero is but it isn't a fruit. - What is a camero anyway? - - You're right that apple is a fruit, but there's one you're missing. Also, mushroom is not a fruit. - You're right that grape is a fruit, but there's one you're missing. Also, mushroom is not a fruit. - - - - - - - Banana - No, sorry, a banana is a fruit. - poor banana. - - Ice Cream - Mushroom - Mushroom is a fungus, not a vegetable. - You're right that mushrooms aren't vegetables. - - Brussel Sprout - Brussel sprouts are vegetables. - Brussel sprout is the only vegetable in this list. - - Making a banana split? - That will make a horrible dessert: a brussel sprout split? - - - - """) - - 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(""" - - - - - Apple - You're right that apple is a fruit. - Remember that apple is also a fruit. - - Mushroom - Mushroom is a fungus, not a fruit. - You're right that mushrooms aren't fruit - - Grape - You're right that grape is a fruit - Remember that grape is also a fruit. - - Mustang - Camero - I don't know what a Camero is but it isn't a fruit. - What is a camero anyway? - - You're right that apple is a fruit, but there's one you're missing. Also, mushroom is not a fruit. - You're right that grape is a fruit, but there's one you're missing. Also, mushroom is not a fruit. - - - - - - - Banana - No, sorry, a banana is a fruit. - poor banana. - - Ice Cream - Mushroom - Mushroom is a fungus, not a vegetable. - You're right that mushrooms aren't vegatbles - - Brussel Sprout - Brussel sprouts are vegetables. - Brussel sprout is the only vegetable in this list. - - Making a banana split? - That will make a horrible dessert: a brussel sprout split? - - - - - Hint one. - Hint two. - Hint three. - - - """) - - -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(""" - - - - - Mushroom - Mushroom is a fungus, not a fruit. - - Potato - Apple - Apple is indeed a fruit. - - - - - - - - Mushroom - Mushroom is a fungus, not a vegetable. - - Potato - Potato is a root vegetable. - - Apple - Apple is a fruit. - - - - - """) - - 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(""" - - - - - Mushroom - Mushroom is a fungus, not a fruit. - - Potato - Apple - Apple is indeed a fruit. - - - - - - - - Mushroom - Mushroom is a fungus, not a vegetable. - - Potato - Potato is a root vegetable. - - Apple - Apple is a fruit. - - - - - - 0) spaces on previous line. - 1) roses are red. - 2) where are the lions? - - - """) - - -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(""" - - - - Viva la France! - - - - - - """) - - it 'produces xml with or=', -> - data = MarkdownEditingDescriptor.markdownToXml(""">>Where Paris?<< - = France {{ BRAVO::hint1}} - or= USA {{ meh::hint2 }} - - """) - expect(data).toXMLEqual(""" - - - - hint1 - hint2 - - - - - - - """) - - it 'produces xml with not=', -> - data = MarkdownEditingDescriptor.markdownToXml(""">>Revenge is a dish best served<< - = cold {{khaaaaaan!}} - not= warm {{feedback2}} - - """) - expect(data).toXMLEqual(""" - - - - khaaaaaan! - feedback2 - - - - - - """) - - it 'produces xml with s=', -> - data = MarkdownEditingDescriptor.markdownToXml(""">>q<< - s= 2 {{feedback1}} - - """) - expect(data).toXMLEqual(""" - - - - feedback1 - - - - - - """) - - it 'produces xml with = and or= and not=', -> - data = MarkdownEditingDescriptor.markdownToXml(""">>q<< - = aaa - or= bbb {{feedback1}} - not= no {{feedback2}} - or= ccc - - """) - expect(data).toXMLEqual(""" - - - - feedback1 - - feedback2 - - - - - - - """) - - it 'produces xml with s= and or=', -> - data = MarkdownEditingDescriptor.markdownToXml(""">>q<< - s= 2 {{feedback1}} - or= bbb {{feedback2}} - or= ccc - - """) - expect(data).toXMLEqual(""" - - - - feedback1 - feedback2 - - - - - - - - """) - - it 'produces xml with each = making a new question', -> - data = MarkdownEditingDescriptor.markdownToXml(""" - >>q<< - = aaa - or= bbb - s= ccc - """) - expect(data).toXMLEqual(""" - - - - - - - - - - - """) - - 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(""" - -

paragraph

- - - - - - - - -

paragraph 2

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

paragraph

- -

or= aaa

-

paragraph 2

-
- """) - - 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(""" - - - - - feedback1 - - - - - feedback2 - - - - """) - - 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(""" - - - - hint1 - - - - - There are actually two countries with cities named Paris. - Paris is the capital of one of those countries. - - """) - - -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(""" - - - - - - Pie for everyone! - - - - - - - No pie for you! - - - - - - - - """) - - # 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(""" - - - - - hint1 - - - - - hint2 - - - - hintA - hintB - - - """) - - -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(""" - - - - - A - aaa - bbb - - B - d. - c - - A*B hint - - - - - - - part one, and part two - - - - - hello hint - - - - - - - AA - hint1 - - BB - hint2 - - CC - hint3 - - - - - - - - - - - - - - - aaa - bbb - ccc - - - """) - -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(""" - - - - - Ø - Ø - - BB - - - - - Ø - - - """) - - it 'produces xml with quote-type characters', -> - data = MarkdownEditingDescriptor.markdownToXml(""" - >>"quotes" aren't `fun`<< - () "hello" {{ isn't }} - (x) "isn't" {{ "hello" }} - - """) - expect(data).toXMLEqual(""" - - - - - "hello" - isn't - - "isn't" - "hello" - - - - - """) - - 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(""" - - - -

this (x)

- - a (hint) - - b - -

that (y)

-
- - -
- """) - - # 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(""" - - - -

this [x]

- - a [square] - b {{ this hint passes through }} - -

that []

-
- - -
- """) - - # 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(""" - - - - - - - - - - - - - """) diff --git a/common/lib/xmodule/xmodule/js/spec/problem/edit_spec_hint.js b/common/lib/xmodule/xmodule/js/spec/problem/edit_spec_hint.js new file mode 100644 index 0000000000..13adad407e --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/problem/edit_spec_hint.js @@ -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(`\ + +

Translation between Dropdown and ________ is straightforward.

+ + + + + + + +

Clowns have funny _________ to make people laugh.

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

Translation between Dropdown and ________ is straightforward.

+ + + + + +
+ + + 0) zero + 1) one + 2) two + +
\ +`); + }); + + 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(`\ + + +

A Question ________ is answered.

+ +
+ + + 0) zero + 1) one + +
\ +`); + }); + + it('produces xml with fewer newlines', function() { + const data = MarkdownEditingDescriptor.markdownToXml(`\ +>>q1<< +[[ (aa) {{ hint1 }} + bb + cc {{ hint2 }} ]]\ +`); + expect(data).toXMLEqual(`\ + + + + + + + + + + + +\ +`); + }); + + it('produces xml even with lots of whitespace', function() { + const data = MarkdownEditingDescriptor.markdownToXml(`\ +>>q1<< +[[ + + + aa {{ hint1 }} + + bb {{ hint2 }} + (cc) + + ]]\ +`); + expect(data).toXMLEqual(`\ + + + + + + + + + + + +\ +`); + }); +}); + +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(`\ + + + + + Apple + You're right that apple is a fruit. + Remember that apple is also a fruit. + + Mushroom + Mushroom is a fungus, not a fruit. + You're right that mushrooms aren't fruit + + Grape + You're right that grape is a fruit + Remember that grape is also a fruit. + + Mustang + Camero + I don't know what a Camero is but it isn't a fruit. + What is a camero anyway? + + You're right that apple is a fruit, but there's one you're missing. Also, mushroom is not a fruit. + You're right that grape is a fruit, but there's one you're missing. Also, mushroom is not a fruit. + + + + + + + Banana + No, sorry, a banana is a fruit. + poor banana. + + Ice Cream + Mushroom + Mushroom is a fungus, not a vegetable. + You're right that mushrooms aren't vegetables. + + Brussel Sprout + Brussel sprouts are vegetables. + Brussel sprout is the only vegetable in this list. + + Making a banana split? + That will make a horrible dessert: a brussel sprout split? + + +\ +`); + }); + + 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(`\ + + + + + Apple + You're right that apple is a fruit. + Remember that apple is also a fruit. + + Mushroom + Mushroom is a fungus, not a fruit. + You're right that mushrooms aren't fruit + + Grape + You're right that grape is a fruit + Remember that grape is also a fruit. + + Mustang + Camero + I don't know what a Camero is but it isn't a fruit. + What is a camero anyway? + + You're right that apple is a fruit, but there's one you're missing. Also, mushroom is not a fruit. + You're right that grape is a fruit, but there's one you're missing. Also, mushroom is not a fruit. + + + + + + + Banana + No, sorry, a banana is a fruit. + poor banana. + + Ice Cream + Mushroom + Mushroom is a fungus, not a vegetable. + You're right that mushrooms aren't vegatbles + + Brussel Sprout + Brussel sprouts are vegetables. + Brussel sprout is the only vegetable in this list. + + Making a banana split? + That will make a horrible dessert: a brussel sprout split? + + + + + Hint one. + Hint two. + Hint three. + +\ +`); + }); +}); + + +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(`\ + + + + + Mushroom + Mushroom is a fungus, not a fruit. + + Potato + Apple + Apple is indeed a fruit. + + + + + + + + Mushroom + Mushroom is a fungus, not a vegetable. + + Potato + Potato is a root vegetable. + + Apple + Apple is a fruit. + + + +\ +`); + }); + + 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(`\ + + + + + Mushroom + Mushroom is a fungus, not a fruit. + + Potato + Apple + Apple is indeed a fruit. + + + + + + + + Mushroom + Mushroom is a fungus, not a vegetable. + + Potato + Potato is a root vegetable. + + Apple + Apple is a fruit. + + + + + + 0) spaces on previous line. + 1) roses are red. + 2) where are the lions? + +\ +`); + }); +}); + + +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(`\ + + + +Viva la France! + + + + +\ +`); + }); + + it('produces xml with or=', function() { + const data = MarkdownEditingDescriptor.markdownToXml(`>>Where Paris?<< += France {{ BRAVO::hint1}} +or= USA {{ meh::hint2 }} +\ +`); + expect(data).toXMLEqual(`\ + + + +hint1 + hint2 + + + + + +\ +`); + }); + + it('produces xml with not=', function() { + const data = MarkdownEditingDescriptor.markdownToXml(`>>Revenge is a dish best served<< += cold {{khaaaaaan!}} +not= warm {{feedback2}} +\ +`); + expect(data).toXMLEqual(`\ + + + +khaaaaaan! + feedback2 + + + + +\ +`); + }); + + it('produces xml with s=', function() { + const data = MarkdownEditingDescriptor.markdownToXml(`>>q<< +s= 2 {{feedback1}} +\ +`); + expect(data).toXMLEqual(`\ + + + +feedback1 + + + + +\ +`); + }); + + 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(`\ + + + +feedback1 + + feedback2 + + + + + +\ +`); + }); + + it('produces xml with s= and or=', function() { + const data = MarkdownEditingDescriptor.markdownToXml(`>>q<< +s= 2 {{feedback1}} +or= bbb {{feedback2}} +or= ccc +\ +`); + expect(data).toXMLEqual(`\ + + + +feedback1 + feedback2 + + + + + + +\ +`); + }); + + it('produces xml with each = making a new question', function() { + const data = MarkdownEditingDescriptor.markdownToXml(`\ +>>q<< += aaa +or= bbb +s= ccc\ +`); + expect(data).toXMLEqual(`\ + + + + + + + + + +\ +`); + }); + + 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(`\ + +

paragraph

+ + + + + + + + +

paragraph 2

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

paragraph

+ +

or= aaa

+

paragraph 2

+
\ +`); + }); + + 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(`\ + + + + + feedback1 + + + + + feedback2 + + +\ +`); + }); + + 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(`\ + + + +hint1 + + + + + There are actually two countries with cities named Paris. + Paris is the capital of one of those countries. + +`); + }); +}); + + +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(`\ + + + + + + Pie for everyone! + + + + + + + No pie for you! + + + + + + +\ +`); + }); + + // 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(`\ + + + + + hint1 + + + + + hint2 + + + + hintA + hintB + +\ +`); + }); +}); + + +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(`\ + + + + + A + aaa + bbb + + B + d. + c + + A*B hint + + + + + + + part one, and part two + + + + + hello hint + + + + + + + AA + hint1 + + BB + hint2 + + CC + hint3 + + + + + + + + + + + + + + + aaa + bbb + ccc + +\ +`); + }) +); + +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(`\ + + + + + Ø + Ø + + BB + + + + + Ø + +\ +`); + }); + + 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(`\ + + + + + "hello" + isn't + + "isn't" + "hello" + + + +\ +`); + }); + + 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(`\ + + + +

this (x)

+ + a (hint) + + b + +

that (y)

+
+ + +
\ +`); + }); + + // 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(`\ + + + +

this [x]

+ + a [square] + b {{ this hint passes through }} + +

that []

+
+ + +
\ +`); + }); + + // 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(`\ + + + + + + + + + + + +\ +`); + }); +}); diff --git a/common/lib/xmodule/xmodule/js/spec/tabs/edit.coffee b/common/lib/xmodule/xmodule/js/spec/tabs/edit.coffee deleted file mode 100644 index ae73009bf3..0000000000 --- a/common/lib/xmodule/xmodule/js/spec/tabs/edit.coffee +++ /dev/null @@ -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) diff --git a/common/lib/xmodule/xmodule/js/spec/tabs/edit.js b/common/lib/xmodule/xmodule/js/spec/tabs/edit.js new file mode 100644 index 0000000000..03593b8a91 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/tabs/edit.js @@ -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); + }); + }); +}); diff --git a/common/static/coffee/spec/.gitignore b/common/static/coffee/spec/.gitignore deleted file mode 100644 index a6c7c2852d..0000000000 --- a/common/static/coffee/spec/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.js diff --git a/common/static/coffee/spec/jquery.immediateDescendents_spec.coffee b/common/static/coffee/spec/jquery.immediateDescendents_spec.coffee deleted file mode 100644 index d16bc29c20..0000000000 --- a/common/static/coffee/spec/jquery.immediateDescendents_spec.coffee +++ /dev/null @@ -1,26 +0,0 @@ -describe "$.immediateDescendents", -> - beforeEach -> - setFixtures """ -
-
-
-
-
-
-
-
- """ - - @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) \ No newline at end of file diff --git a/common/static/coffee/spec/jquery.immediateDescendents_spec.js b/common/static/coffee/spec/jquery.immediateDescendents_spec.js new file mode 100644 index 0000000000..57a7a2a384 --- /dev/null +++ b/common/static/coffee/spec/jquery.immediateDescendents_spec.js @@ -0,0 +1,33 @@ +describe("$.immediateDescendents", function() { + beforeEach(function() { + setFixtures(`\ +
+
+
+
+
+
+
+
\ +` + ); + + 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); + }); +}); \ No newline at end of file diff --git a/lms/static/coffee/.gitignore b/lms/static/coffee/.gitignore deleted file mode 100644 index a6c7c2852d..0000000000 --- a/lms/static/coffee/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.js diff --git a/lms/static/coffee/spec/calculator_spec.coffee b/lms/static/coffee/spec/calculator_spec.coffee deleted file mode 100644 index 91d69902e1..0000000000 --- a/lms/static/coffee/spec/calculator_spec.coffee +++ /dev/null @@ -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') diff --git a/lms/static/coffee/spec/calculator_spec.js b/lms/static/coffee/spec/calculator_spec.js new file mode 100644 index 0000000000..4f498ab9d3 --- /dev/null +++ b/lms/static/coffee/spec/calculator_spec.js @@ -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')); + }); +}); diff --git a/lms/static/coffee/spec/courseware_spec.coffee b/lms/static/coffee/spec/courseware_spec.coffee deleted file mode 100644 index 129b4308a6..0000000000 --- a/lms/static/coffee/spec/courseware_spec.coffee +++ /dev/null @@ -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 """ -
-
-
-
-
-
-
- """ - @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]]) diff --git a/lms/static/coffee/spec/courseware_spec.js b/lms/static/coffee/spec/courseware_spec.js new file mode 100644 index 0000000000..7450847b25 --- /dev/null +++ b/lms/static/coffee/spec/courseware_spec.js @@ -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(`\ +
+
+
+
+
+
+
\ +` + ); + 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]])); + }); +}); diff --git a/lms/static/coffee/spec/feedback_form_spec.coffee b/lms/static/coffee/spec/feedback_form_spec.coffee deleted file mode 100644 index c821ee5153..0000000000 --- a/lms/static/coffee/spec/feedback_form_spec.coffee +++ /dev/null @@ -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' diff --git a/lms/static/coffee/spec/feedback_form_spec.js b/lms/static/coffee/spec/feedback_form_spec.js new file mode 100644 index 0000000000..7b88cd315f --- /dev/null +++ b/lms/static/coffee/spec/feedback_form_spec.js @@ -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'); + }); + }); +}); diff --git a/lms/static/coffee/spec/helper.coffee b/lms/static/coffee/spec/helper.coffee deleted file mode 100644 index 0595362cec..0000000000 --- a/lms/static/coffee/spec/helper.coffee +++ /dev/null @@ -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' diff --git a/lms/static/coffee/spec/helper.js b/lms/static/coffee/spec/helper.js new file mode 100644 index 0000000000..cec884a90b --- /dev/null +++ b/lms/static/coffee/spec/helper.js @@ -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'); diff --git a/lms/static/coffee/spec/histogram_spec.coffee b/lms/static/coffee/spec/histogram_spec.coffee deleted file mode 100644 index 9616b37223..0000000000 --- a/lms/static/coffee/spec/histogram_spec.coffee +++ /dev/null @@ -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 - ) diff --git a/lms/static/coffee/spec/histogram_spec.js b/lms/static/coffee/spec/histogram_spec.js new file mode 100644 index 0000000000..cb8317f149 --- /dev/null +++ b/lms/static/coffee/spec/histogram_spec.js @@ -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 + } + }); + }) + ); +}); diff --git a/lms/static/coffee/spec/modules/tab_spec.coffee b/lms/static/coffee/spec/modules/tab_spec.coffee deleted file mode 100644 index a664cb1017..0000000000 --- a/lms/static/coffee/spec/modules/tab_spec.coffee +++ /dev/null @@ -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 diff --git a/lms/static/coffee/spec/modules/tab_spec.js b/lms/static/coffee/spec/modules/tab_spec.js new file mode 100644 index 0000000000..3209b52ae6 --- /dev/null +++ b/lms/static/coffee/spec/modules/tab_spec.js @@ -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); + }); + }); +}); diff --git a/lms/static/coffee/spec/requirejs_spec.coffee b/lms/static/coffee/spec/requirejs_spec.coffee deleted file mode 100644 index 369904f683..0000000000 --- a/lms/static/coffee/spec/requirejs_spec.coffee +++ /dev/null @@ -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) diff --git a/lms/static/coffee/spec/requirejs_spec.js b/lms/static/coffee/spec/requirejs_spec.js new file mode 100644 index 0000000000..3f9eafa2ad --- /dev/null +++ b/lms/static/coffee/spec/requirejs_spec.js @@ -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); + }); +});