Please enable JavaScript to use CodeHS

Exercise 1.1.3: Say Hello!

5 points

Exercise
1.1.3: Say Hello!
Switch to Code View
function HtmlAutograder(codeTree, $codeTree) { this.codeTree = codeTree; this.$codeTree = $codeTree; this.tests = []; } HtmlAutograder.prototype.runHtmlAutograder = function() { var autograder = this; var studentsH1s = autograder.$codeTree['index.html'].find('h1'); var hasH1 = studentsH1s.length > 0; this.tests.push({ testName: 'You must have an h1 tag on your page', result: { success: hasH1, message: hasH1 ? 'Great!' : 'Try again!', }, }); var studentsH1sWithText = studentsH1s.filter(function() { return $(this).text().trim() !== ''; }); var hasH1WithText = studentsH1sWithText.length > 0; this.tests.push({ testName: 'Your h1 tag must have text in it.', result: { success: hasH1WithText, message: hasH1WithText ? 'Great!' : 'Try again!', }, solutionOutput: '<h1>Hello, my name is Karel!</h1>', }); var studentsH1sWithHello = studentsH1s.filter(function() { return _.includes($(this).text().toLowerCase(), 'hello'); }); var hasH1WithHello = studentsH1sWithHello.length > 0; this.tests.push({ testName: 'Your h1 tag must have word "Hello" in it.', result: { success: hasH1WithHello, message: hasH1WithHello ? 'Great!' : 'Try again!', }, solutionOutput: '<h1>Hello, my name is Karel!</h1>', }); return this.createTests(); } HtmlAutograder.prototype.createTests = function() { return this.tests.map(function(testCase) { return { test: testCase.testName, success: testCase.result.success, invalid: testCase.invalid, message: testCase.result.message, studentOutput: testCase.studentOutput, solutionOutput: testCase.solutionOutput, showDiff: testCase.showDiff, }; }); } /** * TestCase object * --------------- * Each has a result object, a boolean foundObj specifying if the queried object * was found, and the actual object (if found). * @params * options: dictionary of parameters * testName: name of the test case (required) * testType: string indicating type of test case; see README.txt * (required) * filename: the file being searched or searched for (required) * autograder: the autograder the TestCase belongs to (required) * contents: query string for searching files as text (required for * contentSearch and cssSearch; optional for attrSearch) * selector: tag, id, or class being searched (required for * tagSearch, * classOrIdSearch, and * attrSearch) * obj: object being searched (default searches entire html * page) * count: minimum number of found selectors (defaults to 1) * property: String of CSS property name (required for cssSearch) * multifile: Boolean indicating whether multiple files of the same * type should be searched. Can only be true for content, * tag, classOrId, attr, and CSS searches. (defaults to * false) */ // The constructor for the TestCase object function TestCase(options) { this.testName = options.testName; this.autograder = options.autograder; var filename = options.filename; this.invalid = false; // These two properties will get redefined by hasHelper // this.foundObj should only be set to true if a TestCase was successful and // has a queried object to return // this.obj will be an array if more than one object matches the query this.foundObj = false; this.obj = options.obj; if (this.obj === null) { // The object passed in is null. This is most likely because the previous // test failed. this.result = { success: false, message: 'The object passed to this test was null. ' + 'Did all of your previous tests pass?' }; return; } if (typeof filename === 'undefined') { this.invalidTest(); return; } // The html object of the entire file or all files of the same type this.file = $('<html>'); var text = '' if (options.multifile) { // Get all of the files that match the extension indicated by // filename. for (var file in this.autograder.codeTree) { if (file.substr(file.lastIndexOf('.') + 1) === filename) { text += this.autograder.codeTree[file]; } } if (text !== '') { this.file.html($(text)); // Adjust filename for displaying error messages filename += ' files'; } else { // The extension was invalid or there are no files with it this.invalidTest(); return; } } else { if (!this.hasFileHelper(filename)) { this.result = { success: false, message: 'No file named ' + filename }; return; } text = this.autograder.codeTree[filename]; if (filename.substr(filename.lastIndexOf('.') + 1) === 'html') { this.file.html($(this.autograder.codeTree[filename])); } } // Flag to check if the author passed in an obj to search within var useObj = true; // If no options.obj was passed in, make this.obj the entire page if (typeof this.obj === 'undefined') { useObj = false; } // Default to one when searching for #items if (typeof options.count === 'undefined') { options.count = 1; } // Choose init function for this.result based on required.testType switch(options.testType) { case 'fileSearch': if (options.multifile) { this.invalidTest(); break; } this.result = this.hasFile(filename); break; case 'contentSearch': if (typeof options.contents !== 'undefined') { if (useObj) { this.result = this.objContainsContents(options.contents, this.obj, filename); break; } this.result = this.containsContents(options.contents, text, filename); break; } this.invalidTest(); break; case 'tagSearch': if (typeof options.selector !== 'undefined') { if (useObj) { this.result = this.objHasTags(options.selector, options.count, this.obj, filename); break; } this.result = this.hasTags(options.selector, options.count); break; } this.invalidTest(); break; case 'classOrIdSearch': if (typeof options.selector !== 'undefined') { if (useObj) { this.result = this.hasClassOrIdWrapper(options.selector, this.obj, filename); break; } this.result = this.hasClassOrIdWrapper(options.selector); break; } this.invalidTest(); break; case 'attrSearch': if (typeof options.selector !== 'undefined') { if (useObj) { this.result = this.objHasAttr(options.selector, options.contents, this.obj, filename); break; } } this.invalidTest(); break; case 'cssSearch': if (typeof options.selector !== 'undefined' && typeof options.property !== 'undefined' && typeof options.contents !== 'undefined') { if (useObj) { this.result = this.objCheckCSS(options.selector, options.property, options.contents, this.obj, filename); break; } this.result = this.checkCSS(options.selector, options.property, options.contents, filename); break; } this.invalidTest(); break; default: this.invalidTest(); } } // A function that should be called if a test is invalid TestCase.prototype.invalidTest = function() { this.invalid = true; this.obj = null; this.result = { success: false, message: 'This test is invalid' }; } /** * Init Functions * -------------- * Functions for defining results objects. There are two types: those that * search the entire html page (basic search) and those that search a passed * in object (obj search). Obj search functions have names preceded by 'obj' * and also take an obj parameter. * * Each function returns an object with: * test: A string of the test's name * success: A boolean indicating whether or not the student passed * message: A string containing a 'Passed!' or error message */ // Checks if the student's code contains a file with the name filename. // Since hasFileHelper is called at the beginning of the TestCase constructor, // this test case should always return true. The full method has been written // in the event that the user needs to use this function to generate a test // case. TestCase.prototype.hasFile = function(filename) { if (this.hasFileHelper(filename)) { return { success: true, message: 'Passed!' }; } return { success: false, message: 'No file named ' + filename }; }; // Checks if the student's code file contains the string 'contents' TestCase.prototype.containsContents = function(contents, fileText, filename) { if (containsString(contents, fileText)) { this.foundObj = true; return { success: true, message: 'Passed!' }; } return { success: false, message: filename + ' does not contain ' + contents }; }; // Checks if the obj contains the string 'contents' TestCase.prototype.objContainsContents = function(contents, obj, filename) { // If an array of objects is passed in, assume a previous test already // found the objects in the student's code. if (typeof obj !== 'string' && obj.length > 1) { for (var i = 0; i < obj.length; i++) { if (containsString(contents, obj[i].innerHTML)) { return { success: true, message: 'Passed!' } } } return { success: false, message: objToString(obj) + ' do not contain ' + contents }; } if (this.hasHelper(obj, this.file, true)) { if (containsString(contents, obj[0].innerHTML)) { return { success: true, message: 'Passed!' }; } this.foundObj = false; this.obj = null; return { success: false, message: objToString(obj) + ' does not contain ' + contents }; } return { success: false, message: filename + ' does not contain ' + objToString(obj) }; } // Checks if this.file contains a particular tag TestCase.prototype.hasTags = function(tag, count) { // At this point, this.obj is the entire html page. Calling this.hasHelper // will set this.obj to the queried tags if found. if (this.hasHelper(tag, this.file, true)) { return this.checkTagCountHelper(tag, count, this.file); } return { success: false, message: 'Could not find ' + tag + ' tag' }; }; // Checks if the obj contains count number of tags // filename is only used to generate an error message. TestCase.prototype.objHasTags = function(tag, count, obj, filename) { // If an array of objects is passed in, assume a previous test already // found the objects in the student's code. if (typeof obj !== 'string' && obj.length > 1) { if (this.hasHelper(tag, obj, true)) { return this.checkTagCountHelper(tag, count, obj); } return { success: false, message: 'Could not find ' + tag + ' tag in ' + objToString(obj) }; } // At this point, this.obj is the object passed in by the author. The first // call to this.hasHelper will just set this.obj to itself. The second sets // it to the queried tags if found. if (this.hasHelper(obj, this.file, true)) { var searchObj = this.obj; if (this.hasHelper(tag, searchObj, true)) { return this.checkTagCountHelper(tag, count, searchObj); } return { success: false, message: 'Could not find ' + tag + ' tag in ' + objToString(obj) }; } return { success: false, message: 'Could not find ' + objToString(obj) + ' in ' + filename }; }; // Wrapper for class/ID search functions. Uses obj to determine which function // to call. Checks if the passed in attrName is valid. TestCase.prototype.hasClassOrIdWrapper = function(attrName, obj, filename) { // Determine whether the user is looking for a class or ID var firstChar = attrName.charAt(0); var attr = ''; if (firstChar === '#') { attr = 'ID'; } else if (firstChar === '.') { attr = 'class'; } if (attr !== '') { if (typeof obj === 'undefined') { return this.hasClassOrId(attrName, attr); } return this.objHasClassOrId(attrName, attr, obj, filename); } this.invalid = true; this.obj = null; return { success: false, message: 'Invalid search string: ' + attrName + '. Not an ID or class.' }; }; // Checks if an obj has the given attribute TestCase.prototype.objHasAttr = function(attr, contents, obj, filename) { // If an array of objects is passed in, assume a previous test already // found the objects in the student's code. if (typeof obj !== 'string' && obj.length > 1) { for (var i = 0; i < obj.length; i++) { var attribute = $(obj[i]).attr(attr); if (typeof attribute !== 'undefined') { if (typeof contents !== 'undefined') { if (attribute === contents) { return { success: true, message: 'Passed!' }; } } else { return { success: true, message: 'Passed!' }; } } } return { success: false, message: 'Could not find ' + attr + ' with value ' + contents }; } if (this.hasHelper(obj, this.file, true)) { var attribute = obj.attr(attr); if (typeof attribute !== 'undefined') { if (typeof contents !== 'undefined') { if (attribute === contents) { return { success: true, message: 'Passed!' }; } return { success: false, message: 'Value of ' + attr + ' is not ' + contents }; } return { success: true, message: 'Passed!' }; } this.foundObj = false; this.obj = null; return { success: false, message: objToString(obj) + ' does not have the attribute ' + attr }; } return { success: false, message: 'Could not find ' + objToString(obj) + ' in ' + filename }; }; // Checks that the objects matching the 'selector' have a 'property' with the // value 'contents.' TestCase.prototype.checkCSS = function(selector, property, contents, filename) { // Check selector validity in file before checking iframe if (this.hasHelper(selector, this.file, true)) { return this.cssHelper(selector, selector, property, contents); } return { success: false, message: 'No element matching ' + selector + ' could be found in ' + filename }; }; // Checks that obj matches the selector, which must be a class or ID, and that the obj // has the CSS 'property' with value 'contents.' TestCase.prototype.objCheckCSS = function(selector, property, contents, obj, filename) { // Check obj validity in file if (this.hasHelper(obj, this.file, true)) { // Check if obj has selector before checking iframe if (this.hasHelper(selector, obj, false)) { var select = objToString(obj) + selector; return this.cssHelper(objToString(obj), select, property, contents); } return { success: false, message: objToString(obj) + ' does not match ' + selector }; } return { success: false, message: objToString(obj) + ' not found in ' + filename }; }; /** * Helper Functions * ---------------- * Functions called by the init functions. */ // A helper function for using jquery-expect to make CSS assertions. Inspects a // hidden iframe in the Grading tab to find the selector. Then checks if the // selector has the given property with value contents. TestCase.prototype.cssHelper = function(errorString, selector, property, contents) { var iframe = $('#hidden-frame').contents(); var query = iframe.find(selector); // This should always be true since hasHelper was called before-hand. if (query.length > 0) { try { $expect(query).has.css(property, contents); } catch (oException) { this.foundObj = false; this.obj = null; return { success: false, message: errorString + ' does not have ' + property + ' ' + contents }; } this.obj = query; return { success: true, message: 'Passed!' }; } // This case indicates that the item was found in the student's code but not // in the iframe. It should not reach here unless something went wrong with // the TestCase. this.foundObj = false; this.obj = null; return { success: false, message: errorString + ' did not render' }; } // Looks for count number of tags in obj TestCase.prototype.checkTagCountHelper = function(tag, count, obj) { try { $expect(obj.find(tag)).to.be.above(count - 1); } catch (oException) { this.foundObj = false; this.obj = null; return { success: false, message: 'Found less than ' + count + ' ' + tag + ' tags' }; } return { success: true, message: 'Passed!' }; }; // Checks if this.file contains a particular class or ID indicated by attrName TestCase.prototype.hasClassOrId = function(attrName, attr) { // At this point, this.obj is the entire html page. Calling this.hasHelper // will set this.obj to element matching attrName if found. if (this.hasHelper(attrName, this.file, true)) { return { success: true, message: 'Passed!' }; } return { success: false, message: 'Could not find element with ' + attr + ' ' + attrName.substring(1) }; }; // Checks if the obj contains an element matching attrName // obj and filename are only used to generate an error message. TestCase.prototype.objHasClassOrId = function(attrName, attr, obj, filename) { // If an array of objects is passed in, assume a previous test already // found the objects in the student's code. if (typeof obj !== 'string' && obj.length > 1) { if (this.hasHelper(attrName, obj, false)) { return { success: true, message: 'Passed!' }; } return { success: false, message: objToString(obj) + ' does not have ' + attr + ' ' + attrName.substring(1) }; } // At this point, this.obj is the object passed in by the author. Both calls // to this.hasHelper set this.obj to itself as long as it is found and it // matches attrName. if (this.hasHelper(obj, this.file, true)) { if (this.hasHelper(attrName, this.obj, false)) { return { success: true, message: 'Passed!' }; } // this.obj does not match attrName return { success: false, message: objToString(obj) + ' does not have ' + attr + ' ' + attrName.substring(1) }; } return { success: false, message: 'Could not find ' + objToString(obj) + ' in ' + filename }; }; // Returns if a file with the name filename exists in the autograder's codeTree TestCase.prototype.hasFileHelper = function(filename) { return (filename in this.autograder.codeTree); }; // Returns if the query can be found within obj if isChild is true // Returns if the query applies to the obj if isChild is false // Uses jquery-expect if the query is a string. TestCase.prototype.hasHelper = function(query, obj, isChild) { if (typeof query === 'string') { var found = obj; try { if (isChild) { $expect(obj).to.have(query); found = obj.find(query); } else { $expect(obj).to.be(query); } } catch (oException) { this.foundObj = false; this.obj = null; return false; } this.foundObj = true; this.obj = found; return true; } // Both query and obj are Objects var objText = obj[0].innerHTML; var container = $('<div>'); container.append(query); var queryText = container[0].innerHTML; if (containsString(queryText, objText)) { this.foundObj = true; this.obj = query; return true; } this.foundObj = false; this.obj = null; return false; }; // Checks if the query string is in str function containsString(query, str) { return (str.indexOf(query) > -1); } // Converts an object to a string for printing error messages function objToString(obj) { if (typeof obj === 'string') { return obj; } return obj.selector; }