Files
edx-platform/cms/static/js/spec/views/metadata_edit_spec.js
muhammad-ammar 1c53360206 basic tab attach uploads with edx_video_id
EDUCATOR-2761
2018-05-02 18:25:43 +05:00

741 lines
28 KiB
JavaScript

/*
* decaffeinate suggestions:
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
define(["underscore", "js/models/metadata", "js/collections/metadata", "js/views/metadata", "cms/js/main",
"js/views/video/transcripts/utils", 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'],
function(_, MetadataModel, MetadataCollection, MetadataView, main, TranscriptUtils, AjaxHelpers) {
const verifyInputType = function(input, expectedType) {
// Some browsers (e.g. FireFox) do not support the "number"
// input type. We can accept a "text" input instead
// and still get acceptable behavior in the UI.
if ((expectedType === 'number') && (input.type !== 'number')) {
expectedType = 'text';
}
expect(input.type).toBe(expectedType);
};
describe("Test Metadata Editor", function() {
const editorTemplate = readFixtures('metadata-editor.underscore');
const numberEntryTemplate = readFixtures('metadata-number-entry.underscore');
const stringEntryTemplate = readFixtures('metadata-string-entry.underscore');
const optionEntryTemplate = readFixtures('metadata-option-entry.underscore');
const listEntryTemplate = readFixtures('metadata-list-entry.underscore');
const dictEntryTemplate = readFixtures('metadata-dict-entry.underscore');
beforeEach(function() {
setFixtures($("<script>", {id: "metadata-editor-tpl", type: "text/template"}).text(editorTemplate));
appendSetFixtures($("<script>", {id: "metadata-number-entry", type: "text/template"}).text(numberEntryTemplate));
appendSetFixtures($("<script>", {id: "metadata-string-entry", type: "text/template"}).text(stringEntryTemplate));
appendSetFixtures($("<script>", {id: "metadata-option-entry", type: "text/template"}).text(optionEntryTemplate));
appendSetFixtures($("<script>", {id: "metadata-list-entry", type: "text/template"}).text(listEntryTemplate));
appendSetFixtures($("<script>", {id: "metadata-dict-entry", type: "text/template"}).text(dictEntryTemplate));
});
const genericEntry = {
default_value: 'default value',
display_name: "Display Name",
explicitly_set: true,
field_name: "display_name",
help: "Specifies the name for this component.",
options: [],
type: MetadataModel.GENERIC_TYPE,
value: "Word cloud"
};
const videoIDEntry = _.extend({}, genericEntry, {field_name: "edx_video_id", type: "VideoID"});
const selectEntry = {
default_value: "answered",
display_name: "Show Answer",
explicitly_set: false,
field_name: "show_answer",
help: "When should you show the answer",
options: [
{"display_name": "Always", "value": "always"},
{"display_name": "Answered", "value": "answered"},
{"display_name": "Never", "value": "never"}
],
type: MetadataModel.SELECT_TYPE,
value: "always"
};
const integerEntry = {
default_value: 6,
display_name: "Inputs",
explicitly_set: false,
field_name: "num_inputs",
help: "Number of text boxes for student to input words/sentences.",
options: {min: 1},
type: MetadataModel.INTEGER_TYPE,
value: 5
};
const floatEntry = {
default_value: 2.7,
display_name: "Weight",
explicitly_set: true,
field_name: "weight",
help: "Weight for this problem",
options: {min: 1.3, max:100.2, step:0.1},
type: MetadataModel.FLOAT_TYPE,
value: 10.2
};
const listEntry = {
default_value: ["a thing", "another thing"],
display_name: "List",
explicitly_set: false,
field_name: "list",
help: "A list of things.",
options: [],
type: MetadataModel.LIST_TYPE,
value: ["the first display value", "the second"]
};
const timeEntry = {
default_value: "00:00:00",
display_name: "Time",
explicitly_set: true,
field_name: "relative_time",
help: "Specifies the name for this component.",
options: [],
type: MetadataModel.RELATIVE_TIME_TYPE,
value: "12:12:12"
};
const dictEntry = {
default_value: {
'en': 'English',
'ru': 'Русский'
},
display_name: "New Dict",
explicitly_set: false,
field_name: "dict",
help: "Specifies the name for this component.",
type: MetadataModel.DICT_TYPE,
value: {
'en': 'English',
'ru': 'Русский',
'ua': 'Українська',
'fr': 'Français'
}
};
// Test for the editor that creates the individual views.
describe("MetadataView.Editor creates editors for each field", function() {
beforeEach(function() {
this.model = new MetadataCollection(
[
integerEntry,
floatEntry,
selectEntry,
genericEntry,
{
default_value: null,
display_name: "Unknown",
explicitly_set: true,
field_name: "unknown_type",
help: "Mystery property.",
options: [
{"display_name": "Always", "value": "always"},
{"display_name": "Answered", "value": "answered"},
{"display_name": "Never", "value": "never"}],
type: "unknown type",
value: null
},
listEntry,
timeEntry,
dictEntry
]
);
});
it("creates child views on initialize, and sorts them alphabetically", function() {
const view = new MetadataView.Editor({collection: this.model});
const childModels = view.collection.models;
expect(childModels.length).toBe(8);
// Be sure to check list view as well as other input types
const childViews = view.$el.find('.setting-input, .list-settings');
expect(childViews.length).toBe(8);
const verifyEntry = function(index, display_name, type) {
expect(childModels[index].get('display_name')).toBe(display_name);
verifyInputType(childViews[index], type);
};
verifyEntry(0, 'Display Name', 'text');
verifyEntry(1, 'Inputs', 'number');
verifyEntry(2, 'List', '');
verifyEntry(3, 'New Dict', '');
verifyEntry(4, 'Show Answer', 'select-one');
verifyEntry(5, 'Time', 'text');
verifyEntry(6, 'Unknown', 'text');
verifyEntry(7, 'Weight', 'number');
});
it("returns its display name", function() {
const view = new MetadataView.Editor({collection: this.model});
expect(view.getDisplayName()).toBe("Word cloud");
});
it("returns an empty string if there is no display name property with a valid value", function() {
let view = new MetadataView.Editor({collection: new MetadataCollection()});
expect(view.getDisplayName()).toBe("");
view = new MetadataView.Editor({collection: new MetadataCollection([
{
default_value: null,
display_name: "Display Name",
explicitly_set: false,
field_name: "display_name",
help: "",
options: [],
type: MetadataModel.GENERIC_TYPE,
value: null
}])
});
expect(view.getDisplayName()).toBe("");
});
it("has no modified values by default", function() {
const view = new MetadataView.Editor({collection: this.model});
expect(view.getModifiedMetadataValues()).toEqual({});
});
it("returns modified values only", function() {
const view = new MetadataView.Editor({collection: this.model});
const childModels = view.collection.models;
childModels[0].setValue('updated display name');
childModels[1].setValue(20);
expect(view.getModifiedMetadataValues()).toEqual({
display_name : 'updated display name',
num_inputs: 20
});
});
});
// Tests for individual views.
const assertInputType = function(view, expectedType) {
const input = view.$el.find('.setting-input');
expect(input.length).toEqual(1);
verifyInputType(input[0], expectedType);
};
const assertValueInView = (view, expectedValue) => expect(view.getValueFromEditor()).toEqual(expectedValue);
const assertCanUpdateView = function(view, newValue) {
view.setValueInEditor(newValue);
expect(view.getValueFromEditor()).toEqual(newValue);
};
const assertClear = function(view, modelValue, editorValue) {
if (editorValue == null) { editorValue = modelValue; }
view.clear();
expect(view.model.getValue()).toBe(null);
expect(view.model.getDisplayValue()).toEqual(modelValue);
expect(view.getValueFromEditor()).toEqual(editorValue);
};
const assertUpdateModel = function(view, originalValue, newValue) {
view.setValueInEditor(newValue);
expect(view.model.getValue()).toEqual(originalValue);
view.updateModel();
expect(view.model.getValue()).toEqual(newValue);
};
describe("MetadataView.String is a basic string input with clear functionality", function() {
beforeEach(function() {
const model = new MetadataModel(genericEntry);
this.view = new MetadataView.String({model});
});
it("uses a text input type", function() {
assertInputType(this.view, 'text');
});
it("returns the intial value upon initialization", function() {
assertValueInView(this.view, 'Word cloud');
});
it("can update its value in the view", function() {
assertCanUpdateView(this.view, "updated ' \" &");
});
it("has a clear method to revert to the model default", function() {
assertClear(this.view, 'default value');
});
it("has an update model method", function() {
assertUpdateModel(this.view, 'Word cloud', 'updated');
});
});
describe("MetadataView.VideoID", function() {
var waitForMock;
waitForMock = function(mock) {
return jasmine.waitUntil(function() {
return mock.calls.count() === 1;
});
};
beforeEach(function() {
const model = new MetadataModel(videoIDEntry);
spyOn(TranscriptUtils.Storage, 'set');
this.view = new MetadataView.VideoID({model});
spyOn(Backbone, 'trigger');
expect(TranscriptUtils.Storage.set).toHaveBeenCalledWith('edx_video_id', this.view.getValueFromEditor());
});
it("triggers correct event on input change", function(done) {
// change value and trigger input event
this.view.$el.find('input').val("1234-5678-90").trigger('input');
waitForMock(Backbone.trigger)
.then(function() {
expect(Backbone.trigger).toHaveBeenCalledWith('transcripts:basicTabFieldChanged');
})
.always(done);
});
it("triggers correct event on clear", function(done) {
this.view.clear();
waitForMock(Backbone.trigger)
.then(function() {
expect(Backbone.trigger).toHaveBeenCalledWith('transcripts:basicTabFieldChanged');
})
.always(done);
});
it("constructs correct data", function() {
expect(
this.view.getData()
).toEqual(
[{mode: 'edx_video_id', type: 'edx_video_id', video: this.view.getValueFromEditor()}]
);
});
});
describe("MetadataView.Option is an option input type with clear functionality", function() {
beforeEach(function() {
const model = new MetadataModel(selectEntry);
this.view = new MetadataView.Option({model});
});
it("uses a select input type", function() {
assertInputType(this.view, 'select-one');
});
it("returns the intial value upon initialization", function() {
assertValueInView(this.view, 'always');
});
it("can update its value in the view", function() {
assertCanUpdateView(this.view, "never");
});
it("has a clear method to revert to the model default", function() {
assertClear(this.view, 'answered');
});
it("has an update model method", function() {
assertUpdateModel(this.view, null, 'never');
});
it("does not update to a value that is not an option", function() {
this.view.setValueInEditor("not an option");
expect(this.view.getValueFromEditor()).toBe('always');
});
});
describe("MetadataView.Number supports integer or float type and has clear functionality", function() {
const verifyValueAfterChanged = function(view, value, expectedResult) {
view.setValueInEditor(value);
view.changed();
expect(view.getValueFromEditor()).toBe(expectedResult);
};
beforeEach(function() {
const integerModel = new MetadataModel(integerEntry);
this.integerView = new MetadataView.Number({model: integerModel});
const floatModel = new MetadataModel(floatEntry);
this.floatView = new MetadataView.Number({model: floatModel});
});
it("uses a number input type", function() {
assertInputType(this.integerView, 'number');
assertInputType(this.floatView, 'number');
});
it("returns the intial value upon initialization", function() {
assertValueInView(this.integerView, '5');
assertValueInView(this.floatView, '10.2');
});
it("can update its value in the view", function() {
assertCanUpdateView(this.integerView, "12");
assertCanUpdateView(this.floatView, "-2.4");
});
it("has a clear method to revert to the model default", function() {
assertClear(this.integerView, 6, '6');
assertClear(this.floatView, 2.7, '2.7');
});
it("has an update model method", function() {
assertUpdateModel(this.integerView, null, '90');
assertUpdateModel(this.floatView, 10.2, '-9.5');
});
it("knows the difference between integer and float", function() {
expect(this.integerView.isIntegerField()).toBeTruthy();
expect(this.floatView.isIntegerField()).toBeFalsy();
});
it("sets attribtues related to min, max, and step", function() {
const verifyAttributes = function(view, min, step, max=null) {
const inputEntry = view.$el.find('input');
expect(Number(inputEntry.attr('min'))).toEqual(min);
expect(Number(inputEntry.attr('step'))).toEqual(step);
if (max === !null) {
expect(Number(inputEntry.attr('max'))).toEqual(max);
}
};
verifyAttributes(this.integerView, 1, 1);
verifyAttributes(this.floatView, 1.3, .1, 100.2);
});
it("corrects values that are out of range", function() {
verifyValueAfterChanged(this.integerView, '-4', '1');
verifyValueAfterChanged(this.integerView, '1', '1');
verifyValueAfterChanged(this.integerView, '0', '1');
verifyValueAfterChanged(this.integerView, '3001', '3001');
verifyValueAfterChanged(this.floatView, '-4', '1.3');
verifyValueAfterChanged(this.floatView, '1.3', '1.3');
verifyValueAfterChanged(this.floatView, '1.2', '1.3');
verifyValueAfterChanged(this.floatView, '100.2', '100.2');
verifyValueAfterChanged(this.floatView, '100.3', '100.2');
});
it("sets default values for integer and float fields that are empty", function() {
verifyValueAfterChanged(this.integerView, '', '6');
verifyValueAfterChanged(this.floatView, '', '2.7');
});
it("disallows invalid characters", function() {
const verifyValueAfterKeyPressed = function(view, character, reject) {
const event = {
type : 'keypress',
which : character.charCodeAt(0),
keyCode: character.charCodeAt(0),
preventDefault() { return 'no op'; }
};
spyOn(event, 'preventDefault');
view.$el.find('input').trigger(event);
if (reject) {
expect(event.preventDefault).toHaveBeenCalled();
} else {
expect(event.preventDefault).not.toHaveBeenCalled();
}
};
const verifyDisallowedChars = function(view) {
verifyValueAfterKeyPressed(view, 'a', true);
verifyValueAfterKeyPressed(view, '.', view.isIntegerField());
verifyValueAfterKeyPressed(view, '[', true);
verifyValueAfterKeyPressed(view, '@', true);
[0, 1, 2, 3, 4, 5, 6, 7, 8].map((i) =>
verifyValueAfterKeyPressed(view, String(i), false));
};
verifyDisallowedChars(this.integerView);
verifyDisallowedChars(this.floatView);
});
});
describe("MetadataView.List allows the user to enter an ordered list of strings", function() {
beforeEach(function() {
const listModel = new MetadataModel(listEntry);
this.listView = new MetadataView.List({model: listModel});
this.el = this.listView.$el;
main();
});
it("returns the initial value upon initialization", function() {
assertValueInView(this.listView, ['the first display value', 'the second']);
});
it("updates its value correctly", function() {
assertCanUpdateView(this.listView, ['a new item', 'another new item', 'a third']);
});
it("has a clear method to revert to the model default", function() {
this.el.find('.create-setting').click();
assertClear(this.listView, ['a thing', 'another thing']);
expect(this.el.find('.create-setting')).not.toHaveClass('is-disabled');
});
it("has an update model method", function() {
assertUpdateModel(this.listView, null, ['a new value']);
});
it("can add an entry", function() {
expect(this.listView.model.get('value').length).toEqual(2);
this.el.find('.create-setting').click();
expect(this.el.find('input.input').length).toEqual(3);
});
it("can remove an entry", function() {
expect(this.listView.model.get('value').length).toEqual(2);
this.el.find('.remove-setting').first().click();
expect(this.listView.model.get('value').length).toEqual(1);
});
it("only allows one blank entry at a time", function() {
expect(this.el.find('input').length).toEqual(2);
this.el.find('.create-setting').click();
this.el.find('.create-setting').click();
expect(this.el.find('input').length).toEqual(3);
});
it("re-enables the add setting button after entering a new value", function() {
expect(this.el.find('input').length).toEqual(2);
this.el.find('.create-setting').click();
expect(this.el.find('.create-setting')).toHaveClass('is-disabled');
this.el.find('input').last().val('third setting');
this.el.find('input').last().trigger('input');
expect(this.el.find('.create-setting')).not.toHaveClass('is-disabled');
});
});
describe("MetadataView.RelativeTime allows the user to enter time string in HH:mm:ss format", function() {
beforeEach(function() {
const model = new MetadataModel(timeEntry);
this.view = new MetadataView.RelativeTime({model});
});
it("uses a text input type", function() {
assertInputType(this.view, 'text');
});
it("returns the intial value upon initialization", function() {
assertValueInView(this.view, '12:12:12');
});
it("value is converted correctly", function() {
const { view } = this;
const cases = [
{
input: '23:100:0',
output: '23:59:59'
},
{
input: '100000000000000000',
output: '23:59:59'
},
{
input: '80000',
output: '22:13:20'
},
{
input: '-100',
output: '00:00:00'
},
{
input: '-100:-10',
output: '00:00:00'
},
{
input: '99:99',
output: '01:40:39'
},
{
input: '2',
output: '00:00:02'
},
{
input: '1:2',
output: '00:01:02'
},
{
input: '1:25',
output: '00:01:25'
},
{
input: '3:1:25',
output: '03:01:25'
},
{
input: ' 2 3 : 5 9 : 5 9 ',
output: '23:59:59'
},
{
input: '9:1:25',
output: '09:01:25'
},
{
input: '77:72:77',
output: '23:59:59'
},
{
input: '22:100:100',
output: '23:41:40'
},
// negative value
{
input: '-22:22:22',
output: '00:22:22'
},
// simple string
{
input: 'simple text',
output: '00:00:00'
},
{
input: 'a10a:a10a:a10a',
output: '00:00:00'
},
// empty string
{
input: '',
output: '00:00:00'
}
];
$.each(cases, (index, data) => expect(view.parseRelativeTime(data.input)).toBe(data.output));
});
it("can update its value in the view", function() {
assertCanUpdateView(this.view, "23:59:59");
this.view.setValueInEditor("33:59:59");
this.view.updateModel();
assertValueInView(this.view, "23:59:59");
});
it("has a clear method to revert to the model default", function() {
assertClear(this.view, '00:00:00');
});
it("has an update model method", function() {
assertUpdateModel(this.view, '12:12:12', '23:59:59');
});
});
describe("MetadataView.Dict allows the user to enter key-value pairs of strings", function() {
beforeEach(function() {
const dictModel = new MetadataModel($.extend(true, {}, dictEntry));
this.dictView = new MetadataView.Dict({model: dictModel});
this.el = this.dictView.$el;
main();
});
it("returns the initial value upon initialization", function() {
assertValueInView(this.dictView, {
'en': 'English',
'ru': 'Русский',
'ua': 'Українська',
'fr': 'Français'
});
});
it("updates its value correctly", function() {
assertCanUpdateView(this.dictView, {
'ru': 'Русский',
'ua': 'Українська',
'fr': 'Français'
});
});
it("has a clear method to revert to the model default", function() {
this.el.find('.create-setting').click();
assertClear(this.dictView, {
'en': 'English',
'ru': 'Русский'
});
expect(this.el.find('.create-setting')).not.toHaveClass('is-disabled');
});
it("has an update model method", function() {
assertUpdateModel(this.dictView, null, {'fr': 'Français'});
});
it("can add an entry", function() {
expect(_.keys(this.dictView.model.get('value')).length).toEqual(4);
this.el.find('.create-setting').click();
expect(this.el.find('input.input-key').length).toEqual(5);
});
it("can remove an entry", function() {
expect(_.keys(this.dictView.model.get('value')).length).toEqual(4);
this.el.find('.remove-setting').first().click();
expect(_.keys(this.dictView.model.get('value')).length).toEqual(3);
});
it("only allows one blank entry at a time", function() {
expect(this.el.find('input.input-key').length).toEqual(4);
this.el.find('.create-setting').click();
this.el.find('.create-setting').click();
expect(this.el.find('input.input-key').length).toEqual(5);
});
it("only allows unique keys", function() {
const data = [
{
expectedValue: {'ru': 'Русский'},
initialValue: {'ru': 'Русский'},
testValue: {
'key': 'ru',
'value': ''
}
},
{
expectedValue: {'ru': 'Русский'},
initialValue: {'ru': 'Some value'},
testValue: {
'key': 'ru',
'value': 'Русский'
}
},
{
expectedValue: {'ru': 'Русский'},
initialValue: {'ru': 'Русский'},
testValue: {
'key': '',
'value': ''
}
}
];
_.each(data, ((d, index) => {
this.dictView.setValueInEditor(d.initialValue);
this.dictView.updateModel();
this.el.find('.create-setting').click();
const item = this.el.find('.list-settings-item').last();
item.find('.input-key').val(d.testValue.key);
item.find('.input-value').val(d.testValue.value);
expect(this.dictView.getValueFromEditor()).toEqual(d.expectedValue);
})
);
});
it("re-enables the add setting button after entering a new value", function() {
expect(this.el.find('input.input-key').length).toEqual(4);
this.el.find('.create-setting').click();
expect(this.el.find('.create-setting')).toHaveClass('is-disabled');
this.el.find('input.input-key').last().val('third setting');
this.el.find('input.input-key').last().trigger('input');
expect(this.el.find('.create-setting')).not.toHaveClass('is-disabled');
});
});
});
});