Commit 386029be by Julian Arni

Adding jasmine tests; code cleanup.

parent b03d9390
<section id="inputtype_${id}" class="jsinput" <section id="inputtype_${id}" class="jsinput"
data="${gradefn}" data="${gradefn}"
% if saved_state: % if saved_state:
data-stored="${saved_state|x}" data-stored="${saved_state|x}"
% endif % endif
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
% endif % endif
> >
<div class="script_placeholder" data-src="${applet_loader}"/> <div class="script_placeholder" data-src="${applet_loader}"/>
% if status == 'unsubmitted': % if status == 'unsubmitted':
<div class="unanswered" id="status_${id}"> <div class="unanswered" id="status_${id}">
...@@ -23,8 +23,8 @@ ...@@ -23,8 +23,8 @@
<div class="incorrect" id="status_${id}"> <div class="incorrect" id="status_${id}">
% endif % endif
<iframe name="iframe_${id}" <iframe name="iframe_${id}"
id="iframe_${id}" id="iframe_${id}"
sandbox="allow-scripts allow-popups allow-same-origin allow-forms allow-pointer-lock" sandbox="allow-scripts allow-popups allow-same-origin allow-forms allow-pointer-lock"
seamless="seamless" seamless="seamless"
frameborder="0" frameborder="0"
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
height="${height}" height="${height}"
width="${width}" width="${width}"
/> />
<input type="hidden" name="input_${id}" id="input_${id}" <input type="hidden" name="input_${id}" id="input_${id}"
waitfor="" waitfor=""
value="${value|h}"/> value="${value|h}"/>
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div> </div>
% endif % endif
% if msg: % if msg:
<span class="message">${msg|n}</span> <span class="message">${msg|n}</span>
% endif % endif
......
...@@ -16,8 +16,16 @@ h2 { ...@@ -16,8 +16,16 @@ h2 {
} }
} }
iframe[seamless]{
background-color: transparent;
border: 0px none transparent;
padding: 0px;
overflow: hidden;
}
.inline-error { .inline-error {
color: darken($error-red, 10%); color: darken($error-red, 11%);
} }
......
...@@ -142,7 +142,7 @@ class @Problem ...@@ -142,7 +142,7 @@ class @Problem
# off @answers # off @answers
check_waitfor: => check_waitfor: =>
for inp in @inputs for inp in @inputs
if ($(inp).attr("waitfor")?) if ($(inp).is("input[waitfor]"))
try try
$(inp).data("waitfor")() $(inp).data("waitfor")()
catch e catch e
......
...@@ -4,45 +4,66 @@ ...@@ -4,45 +4,66 @@
// most relevantly, jquery). Keep in mind what happens in which context // most relevantly, jquery). Keep in mind what happens in which context
// when modifying this file. // when modifying this file.
/* Check whether there is anything to be done */
// When all the problems are first loaded, we want to make sure the
// constructor only runs once for each iframe; but we also want to make
// sure that if part of the page is reloaded (e.g., a problem is
// submitted), the constructor is called again.
if (!jsinput) {
console.log("hi");
jsinput = {
runs : 1,
arr : [],
exists : function(id) {
jsinput.arr.filter(function(e, i, a) {
return e.id = id;
});
}
};
}
jsinput.runs++;
if ($(document).find('section[class="jsinput"]').length > jsinput.runs) {
return;
}
/* Utils */
jsinput._DEBUG = jsinput._DEBUG || true;
var debuglog = function(text) { if (jsinput._DEBUG) { console.log(text);}};
var eqTimeout = function(fn, pred, time, max) {
var i = 0;
while (pred(fn) && i < max) {
setTimeout(fn, time);
}
return fn;
};
var isUndef = function (e) { return (typeof(e) === 'undefined'); };
// _deepKey and _ctxCall are helper functions used to ensure that gradefn
// etc. can be nested objects (e.g., "firepad.getText") and that when
// called they receive the appropriate objects as "this" (e.g., "firepad").
// Take a string and find the nested object that corresponds to it. E.g.: // Take a string and find the nested object that corresponds to it. E.g.:
// deepKey(obj, "an.example") -> obj["an"]["example"] // deepKey(obj, "an.example") -> obj["an"]["example"]
var _deepKey = function(obj, path){ var _deepKey = function(obj, path){
for (var i = 0, path=path.split('.'), len = path.length; i < len; i++){ for (var i = 0, p=path.split('.'), len = p.length; i < len; i++){
obj = obj[path[i]]; obj = obj[p[i]];
} }
return obj; return obj;
}; };
var _ctxCall = function(obj, fn) {
var func = _deepKey(obj, fn);
var oldthis = fn.split('.');
oldthis.pop();
oldthis = oldthis.join();
var newthis = _deepKey(obj, oldthis);
var args = Array.prototype.slice.call(arguments); /* END Utils */
args = args.slice(2, args.length);
return func.apply(newthis, args);
};
// First time this function was called?
var isFirst = typeof(jsinput.jsinputarr) != 'undefined';
// Use this array to keep track of the elements that have already been
// initialized.
jsinput.jsinputarr = jsinput.jsinputarr || [];
jsinput.jsinputarr.exists = function (id) {
this.filter(function(e, i, a) {
return e.id = id;
});
};
function jsinputConstructor(spec) { function jsinputConstructor(spec) {
// Define an class that will be instantiated for each jsinput element // Define an class that will be instantiated for each jsinput element
...@@ -55,41 +76,29 @@ ...@@ -55,41 +76,29 @@
/* Private methods */ /* Private methods */
var sect = $(spec.elem).parent().find('section[class="jsinput"]'); var sect = $(spec.elem).parent().find('section[class="jsinput"]');
var sectattr = function (e) { return $(sect).attr(e); };
var thisIFrame = $(spec.elem).
find('iframe[name^="iframe_"]').
get(0);
var cWindow = thisIFrame.contentWindow;
// Get the hidden input field to pass to customresponse // Get the hidden input field to pass to customresponse
function inputfield() { function _inputfield() {
var parent = $(spec.elem).parent(); var parent = $(spec.elem).parent();
return parent.find('input[id^="input_"]'); return parent.find('input[id^="input_"]');
} }
var inputfield = _inputfield();
// For the state and grade functions below, use functions instead of
// storing their return values since we might need to call them
// repeatedly, and they might change (e.g., they might not be defined
// when we first try calling them).
// Get the grade function name // Get the grade function name
function getgradefn() { var getgradefn = sectattr("data");
return $(sect).attr("data");
}
// Get state getter // Get state getter
function getgetstate() { var getgetstate = sectattr("data-getstate");
return $(sect).attr("data-getstate");
}
// Get state setter // Get state setter
function getsetstate() { var getsetstate = sectattr("data-setstate");
var gss = $(sect).attr("data-setstate");
return gss;
}
// Get stored state // Get stored state
function getstoredstate() { var getstoredstate = sectattr("data-stored");
return $(sect).attr("data-stored");
}
var thisIFrame = $(spec.elem).
find('iframe[name^="iframe_"]').
get(0);
var cWindow = thisIFrame.contentWindow;
// Put the return value of gradefn in the hidden inputfield. // Put the return value of gradefn in the hidden inputfield.
// If passed an argument, does not call gradefn, and instead directly // If passed an argument, does not call gradefn, and instead directly
...@@ -97,28 +106,32 @@ ...@@ -97,28 +106,32 @@
var update = function (answer) { var update = function (answer) {
var ans; var ans;
ans = _ctxCall(cWindow, gradefn); ans = _deepKey(cWindow, gradefn);
// setstate presumes getstate, so don't getstate unless setstate is // setstate presumes getstate, so don't getstate unless setstate is
// defined. // defined.
if (getgetstate() && getsetstate()) { if (getgetstate && getsetstate) {
var state, store; var state, store;
state = _ctxCall(cWindow, getgetstate()); state = _deepKey(cWindow, getgetstate);
store = { store = {
answer: ans, answer: ans,
state: state state: state
}; };
inputfield().val(JSON.stringify(store));
debuglog("Store: " + store);
inputfield.val(JSON.stringify(store));
} else { } else {
inputfield().val(ans); inputfield.val(ans);
debuglog("Answer: " + ans);
} }
return; return;
}; };
// Find the update button, and bind the update function to its click // Find the update button, and bind the update function to its click
// event. // event.
function updateHandler() { function bindUpdate() {
var updatebutton = $(spec.elem). var updatebutton = $(spec.elem).
find('button[class="update"]').get(0); find('button[class="update"]').
get(0);
$(updatebutton).click(update); $(updatebutton).click(update);
} }
...@@ -130,111 +143,93 @@ ...@@ -130,111 +143,93 @@
/* Initialization */ /* Initialization */
jsinput.jsinputarr.push(that); jsinput.arr.push(that);
// Put the update function as the value of the inputfield's "waitfor" // Put the update function as the value of the inputfield's "waitfor"
// attribute so that it is called when the check button is clicked. // attribute so that it is called when the check button is clicked.
function bindCheck() { function bindCheck() {
inputfield().data('waitfor', that.update); debuglog("Update function: " + that.update);
inputfield.data('waitfor', that.update);
return; return;
} }
var gradefn = getgradefn(); var gradefn = getgradefn;
debuglog("Gradefn: " + gradefn);
if (spec.passive === false) { if (spec.passive === false) {
updateHandler(); // If there is a separate "Update" button, bind update to it.
bindUpdate();
} else {
// Otherwise, bind update to the check button.
bindCheck(); bindCheck();
// Check whether application takes in state and there is a saved }
// state to give it. If getsetstate is specified but calling it
// fails, wait and try again, since the iframe might still be bindCheck();
// loading.
if (getsetstate() && getstoredstate()) { // Check whether application takes in state and there is a saved
var sval; // state to give it. If getsetstate is specified but calling it
if (typeof(getstoredstate()) === "object") { // fails, wait and try again, since the iframe might still be
sval = getstoredstate()["state"]; // loading.
} else { if (getsetstate && getstoredstate) {
sval = getstoredstate(); var sval;
} if (typeof(getstoredstate) === "object") {
function whileloop(n) { sval = getstoredstate["state"];
if (n < 10){ } else {
try { sval = getstoredstate;
_ctxCall(cWindow, getsetstate(), sval); }
} catch (err) {
setTimeout(whileloop(n+1), 200); debuglog("Stored state: "+ sval);
} debuglog("Set_statefn: " + getsetstate);
}
else { function whileloop(n) {
console.log("Error: could not set state"); if (n < 10){
try {
_deepKey(cWindow, getsetstate)(sval);
} catch (err) {
setTimeout(whileloop(n+1), 200);
} }
} }
whileloop(0); else {
debuglog("Error: could not set state");
_deepKey(cWindow, getsetstate)(sval);
}
} }
} else { whileloop(0);
// NOT CURRENTLY SUPPORTED
// If set up to passively receive updates (intercept a function's
// return value whenever the function is called) add an event
// listener that listens to messages that match "that"'s id.
// Decorate the iframe gradefn with updateDecorator.
iframe.contentWindow[gradefn] = updateDecorator(iframe.contentWindow[gradefn]);
iframe.contentWindow.addEventListener('message', function (e) {
var id = e.data[0],
msg = e.data[1];
if (id === spec.id) { update(msg); }
});
} }
return that; return that;
} }
function updateDecorator(fn, id) {
// NOT CURRENTLY SUPPORTED
// Simple function decorator that posts the output of a function to the
// parent iframe before returning the original function's value.
// Can be used to decorate one or more gradefn (instead of using an
// explicit "Update" button) when gradefn is automatically called as part
// of an application's natural behavior.
// The id argument is used to specify which of the instances of jsinput on
// the parent page the message is being posted to.
return function () {
var result = fn.apply(null, arguments);
window.parent.contentWindow.postMessage([id, result], document.referrer);
return result;
};
}
function walkDOM() { function walkDOM() {
// Find all jsinput elements, and create a jsinput object for each one
var all = $(document).find('section[class="jsinput"]');
var newid; var newid;
all.each(function() {
// Find all jsinput elements, and create a jsinput object for each one
var all = $(document).find('section[class="jsinput"]');
all.each(function(index, value) {
// Get just the mako variable 'id' from the id attribute // Get just the mako variable 'id' from the id attribute
newid = $(this).attr("id").replace(/^inputtype_/, ""); newid = $(value).attr("id").replace(/^inputtype_/, "");
if (! jsinput.jsinputarr.exists(newid)){
if (!jsinput.exists(newid)){
var newJsElem = jsinputConstructor({ var newJsElem = jsinputConstructor({
id: newid, id: newid,
elem: this, elem: value,
passive: false passive: true
}); });
} }
}); });
} }
// TODO: Inject css into, and retrieve frame size from, the iframe (for non // This is ugly, but without a timeout pages with multiple/heavy jsinputs
// "seamless"-supporting browsers). // don't load properly.
//var iframeInjection = { if ($.isReady) {
//injectStyles : function (style) { setTimeout(walkDOM, 1000);
//$(document.body).css(style); } else {
//}, $(document).ready(setTimeout(walkDOM, 1000));
//sendMySize : function () { }
//var height = html.height,
//width = html.width; })(window.jsinput = window.jsinput || false);
//window.parent.postMessage(['height', height], '*');
//window.parent.postMessage(['width', width], '*');
//}
//};
setTimeout(walkDOM, 100);
})(window.jsinput = window.jsinput || {})
describe("A jsinput has:", function () {
beforeEach(function () {
$('#fixture').remove();
$.ajax({
async: false,
url: 'mainfixture.html',
success: function(data) {
$('body').append($(data));
}
});
});
describe("The ctxCall function", function() {
it("Evaluatates nested-object functions", function() {
var ctxTest = {
ctxFn : function () {
return this.name ;
}
};
var fnString = "nest.ctxFn";
var holder = {};
holder.nest = ctxTest;
var fn = _ctxCall(holder, fnString);
expect(fnString).toBe(holder.nest.ctxFn());
});
it("Throws an exception when the object does not exits", function () {
var notObj = _ctxCall("twas", "brilling");
expect(notObj).toThrow();
});
it("Throws an exception when the function does not exist", function () {
var anobj = {};
var notFn = _ctxCall("anobj", "brillig");
expect(notFn).toThrow();
});
});
describe("The jsinput constructor", function(){
var testJsElem = jsinputConstructor({
id: 3781,
elem: "<div id='abc'> a div </div>",
passive: false
});
it("Returns an object", function(){
expect(typeof(testJsElem)).toEqual('object');
});
it("Adds the object to the jsinput array", function() {
expect(jsinput.jsinputarr.exists(3781)).toBe(true);
});
describe("The returned object", function() {
it("Has a public 'update' method", function(){
expect(testJsElem.update).toBeDefined();
});
it("Changes the parent's inputfield", function() {
})
});
});
}
)
describe("jsinput test", function () {
beforeEach(function () {
$('#fixture').remove();
$.ajax({
async: false,
url: 'mainfixture.html',
success: function(data) {
$('body').append($(data));
}
});
});
it("")
}
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment