I'm just doing this so I can host the file.
23 | 24 | 29 | 30 |├── .gitignore ├── LICENSE ├── README.md ├── grader.js ├── index.html ├── params.json ├── stylesheets ├── github-light.css ├── normalize.css └── stylesheet.css └── test ├── index.html └── unit_tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Udacity 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grader 2 | Small library for intelligently grading student code and providing lots of feedback 3 | 4 | ## Installation 5 | Just copy grader.js into whatever project/quiz you want to grade. 6 | 7 | HTML/CSS grading depends on [jQuery](https://jquery.com/). Asynchronous testing depends on [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) (you'll need a [polyfill](https://github.com/jakearchibald/es6-promise/) for IE and other unsupported browsers). 8 | 9 | ## Grader Philosophy 10 | 11 | The grader helps you intelligently grade complex problems. In real life, teachers use branching logic to grade student work because even the simplest questions reflect a complex tree of dependent concepts. 12 | 13 | When a teacher examines student work, she starts by grading the highest level aspects of a student's work and then works down to the details. If a student fails to understand a high level concept then there's usually little to gain by traversing down the tree to grade details based upon the higher level concept. 14 | 15 | Teachers also make decisions while they grade. Based on the student's choices, they may want to examine different aspects of their work. For more complex questions, teachers may also grade against multiple questions simultaneously and compare multiple lines of evidence to determine student understanding. 16 | 17 | Most importantly, **great teachers provide as much feedback as possible**. Teachers congratulate students on their successes and point them in the right direction when their understanding strays. 18 | 19 | The grader exists to provide easy branching, checkpoint and feedback logic so you can easily grade student work the same way that classroom teachers grade. 20 | 21 | See the [tips and tricks](#tips-n-tricks) section for grading strategies. 22 | 23 | ## API 24 | 25 | ### Instantiate a new Grader 26 | 27 | ```javascript 28 | var grader = new Grader([type string], [categories object]); 29 | ``` 30 | 31 | There are two options for `type`: `'sync'` or `'async'`. The Grader will run tests synchronously by default. If you have some tests that rely on network requests or other asynchronous actions, set type to `'async'`. Async tests are executed as a series of JavaScript [promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). 32 | 33 | 34 | ```javascript 35 | categories = {category: "message string"} 36 | ``` 37 | 38 | Use the `categories` feedback to set shared feedback that multiple tests may output. This basically exists for the lazy who, when tasked with writing lots of tests with nearly identical feedback for wrong answers, decide to just write the feedback once in the constructor. In the actual tests, just set the `category` in the feedback object to the category you define here. 39 | 40 | ### Adding New Tests 41 | 42 | ```javascript 43 | grader.addTest(callback, feedback object, [keepGoing boolean]); 44 | ``` 45 | 46 | New tests are added to the queue that executes each tests's callback when the previous test's callback resolves to either `true` or `false`. If you are using an async grader, tests can take any amount of time to resolve. 47 | 48 | `callback` must return `true` or `false` indicating whether or not the test passed. You can determine `true` or `false` however you'd like, though there are some [helper functions](#helpers) in `Grader` that exist to make your life a little easier. 49 | 50 | ```javascript 51 | feedback = { 52 | wrongMessage: "mandatory message for a failing test", 53 | comment: "optional string for a passing test", 54 | category: "optional cateogry name" 55 | } 56 | ``` 57 | 58 | `wrongMessage` is mandatory and displayed if the test fails. `comment` is optional and displayed if the test passes. You can set the optional `category` equal to a category set in the Grader constructor if you want to use a general message as feedback for a failing test. If you have both a `category` and a `wrongMessage`, both will be displayed if a test fails. 59 | 60 | If you want the grader to stop grading when a test fails, set `keepGoing` to `false`. It defaults to `true`. 61 | 62 | Being able to stop the `Grader` gives you the ability to maintain a clear picture of the state of the execution environment as you grade. Set `keepGoing` to `false` on any tests examining information on which later tests depend. 63 | 64 | (Why call it `keepGoing`? I initially called it `checkpoint` and set this flag to `true` if I wanted the test to stop if it failed. That felt weird because I was using `true` to say "stop running." I switched to a terminology where I could set the flag equal to `false`, because `false` seems like a better way to say "stop here if something goes wrong!") 65 | 66 | ### Running Tests 67 | 68 | ```javascript 69 | grader.runTests([options object]); 70 | ``` 71 | 72 | Tests do not run automatically. Use `.runTests()` to get them started. 73 | 74 | ```javascript 75 | options = {ignoreCheckpoints: boolean} 76 | ``` 77 | 78 | At the moment, there's only one option, `ignoreCheckpoints`. 79 | 80 | This is purely a development feature. Use `ignoreCheckpoints` to run through every test regardless of any `keepGoing` flags. Great for checking feedback of all of your tests at once. 81 | 82 | ### Pulling Test Results 83 | 84 | ```javascript 85 | grader.onresult = function (result) { ... }; 86 | ``` 87 | 88 | where 89 | 90 | ```javascript 91 | result = { 92 | isCorrect: boolean, 93 | testFeedback: ["array of", "feedback messages", "for wrong answers"], 94 | testComments: ["array of", "feedback messages", "for right answers"] 95 | }; 96 | ``` 97 | 98 | All of the tests must have passed in order for `isCorrect` to be `true`. 99 | 100 | As mentioned earlier, tests execute in either a synchronous or asynchronous queue. Set an `onresult` handler to be called when the queue empties, whether it's because all of the tests executed or the queue was stopped at a checkpoint. 101 | 102 | ### Helper Functions 103 | 104 | All helper functions return `true` if conditions are met and `false` otherwise. 105 | 106 | #### For JavaScript 107 | 108 | ```javascript 109 | grader.isType(value any, expectedType string); 110 | ``` 111 | 112 | `isType` is just a wrapper around `typeof`. 113 | 114 | --- 115 | 116 | ```javascript 117 | grader.isInstance(value any, expectedInstance prototype); 118 | ``` 119 | 120 | This one is easy to confuse. The `expectedInstance` should be an actual instance of the `value`'s prototype. The most common use is for determining if a variable is an array, like so: 121 | 122 | ```javascript 123 | grader.isInstance([1,2,3,4], Array); // true 124 | grader.isInstance("array", Array); // false 125 | grader.isInstance([1,2,3,4], 'array'); // TypeError 126 | ``` 127 | 128 | --- 129 | 130 | ```javascript 131 | grader.isValue(value1 any, value2 any); 132 | ``` 133 | 134 | This helper runs a deep comparison of `value1` and `value2`. 135 | 136 | --- 137 | 138 | ```javascript 139 | grader.isSet(value any); 140 | ``` 141 | 142 | Just checks to make sure the value is not `undefined`. 143 | 144 | --- 145 | 146 | #### For HTML/CSS 147 | 148 | For these methods, assume that `elem` can be either a jQuery element, regular DOM node, or a string selector unless otherwise specified. 149 | 150 | --- 151 | 152 | ```javascript 153 | grader.hasCorrectTag(elem, tag string); 154 | ``` 155 | 156 | Always good to make sure you're looking at the right kind of element. 157 | 158 | --- 159 | 160 | ```javascript 161 | grader.hasCorrectClass(elem, className string); 162 | ``` 163 | 164 | Gotta stay classy ;) 165 | 166 | --- 167 | 168 | ```javascript 169 | grader.hasCorrectId(elem, id string); 170 | ``` 171 | 172 | Does pretty much what you'd expect it to. 173 | 174 | --- 175 | 176 | ```javascript 177 | grader.hasAttr(elem, attrName string, [correctAttr string]); 178 | ``` 179 | 180 | This is called `hasAttr` and not `hasCorrectAttr` because the "correct" aspect is optional. Not all attributes have content, and as such the `correctAttr` is optional. Without the `correctAttr`, the test will pass if the attribute is found, regardless of whether or not it has content. With it, the attribute's content must match `correctAttr`. 181 | 182 | --- 183 | 184 | ```javascript 185 | grader.hasCorrectStyle(elem, cssProperty string, [correctStyle string]); 186 | ``` 187 | 188 | This test pulls a CSS property from an element and compares the style to one or more `correctStyle`. 189 | 190 | --- 191 | 192 | ```javascript 193 | grader.propertyIsLessThan(elem, cssProperty string, value int); 194 | ``` 195 | 196 | This test pulls a CSS property from an element and tests to see if the value of the property is less than the `value` specified. 197 | 198 | --- 199 | 200 | ```javascript 201 | grader.propertyIsGreaterThan(elem, cssProperty string, value int); 202 | ``` 203 | 204 | This test pulls a CSS property from an element and tests to see if the value of the property is greater than the `value` specified. 205 | 206 | --- 207 | 208 | ```javascript 209 | grader.hasCorrectText(elem, text regex); 210 | ``` 211 | 212 | Runs a regex match against the `elem`'s text. If one or more match groups are returned, the test passes. 213 | 214 | --- 215 | 216 | ```javascript 217 | grader.isCorrectElem(elem, correctElem $(element)); 218 | ``` 219 | 220 | This test uses jQuery `.is()` to compare two elements. If they are the same, the test passes. 221 | 222 | --- 223 | 224 | ```javascript 225 | grader.isCorrectCollection(collection, correctCollection $(collection)); 226 | ``` 227 | 228 | Like `isCorrectElem`, this test compares two collections of elements. If they are the same, the test passes. 229 | 230 | --- 231 | 232 | ```javascript 233 | grader.hasCorrectLength(elems, length number); 234 | ``` 235 | 236 | Compares the length of a collection of elements to a number. If the numbers match, the test passes. 237 | 238 | --- 239 | 240 | ```javascript 241 | grader.elemDoesExist(elem); 242 | ``` 243 | 244 | `elem` is generally a string selector with this method. Simply checks to make sure that the selector returns one or more elements. 245 | 246 | --- 247 | 248 | ```javascript 249 | grader.doesExistInParent(elem, parentElem); 250 | ``` 251 | 252 | Both `elem` and `parentElem` can be either strings or jQuery elements. This is a deep child search, meaning the children may be more than one level below the parent. 253 | 254 | --- 255 | 256 | ```javascript 257 | grader.areSiblings(elem1, elem2); 258 | ``` 259 | 260 | If the two elements are siblings, the test passes. This test takes advantage of jQuery's `.siblings()`. 261 | 262 | --- 263 | 264 | ```javascript 265 | grader.isImmediateChild(elem string, parentElem); 266 | ``` 267 | 268 | This is one of two methods where the `elem` needs to be a CSS selector, not a jQuery element. If the `elem` is a child of the `parentElem`, the test passes. 269 | 270 | --- 271 | 272 | ```javascript 273 | grader.hasParent(elem string, parentElem); 274 | ``` 275 | 276 | Like `isImmediateChild`, this method expects a CSS selector for `elem`. This method uses jQuery's `.closest()`, which traverses up the DOM tree until it finds a parent that matches `parentElem`. If if finds a parent, the test passes. 277 | 278 | --- 279 | 280 | ```javascript 281 | grader.sendResultsToExecutor(); 282 | ``` 283 | 284 | Udacity internal only. 285 | 286 | Packages up all test feedback to send to grading code. Use with programming quizzes running on PhantomJS with Karma. 287 | 288 | --- 289 | 290 | ## Examples: 291 | 292 | (Note, these examples may be using older versions of the library so you may see some slight differences in the code. Namely, the `onresult` handler is brand new so you probably won't see it.) 293 | 294 | * [Autocomplete quiz](https://github.com/udacity/course-web-forms/blob/gh-pages/lesson2/quizAutocomplete/grader/execution_files/unit_tests.js) 295 | * [Datalists quiz](https://github.com/udacity/course-web-forms/blob/gh-pages/lesson1/quizDatalists/grader/execution_files/unit_tests.js) 296 | 297 | ## Tips and Tricks 298 | 299 | * Remember, this is just a library. You're writing code, so use whatever logic and constructs you'd like to determine how tests get added, when tests get added, and what kind of feedback students should receive. 300 | * Are there multiple ideas you want to grade independently? Just instantiate multiple Graders! Just make sure you aggregate the feedback before sending it off to grading code. 301 | * Don't be afraid to set `keepGoing` to `false`. Checkpoints exist to help you run tests only when the conditions in the testing environment are correct. This also helps you provide targeted, focused feedback that addresses the current state of the student's code. 302 | * Want to create an optional question? Use if/else logic to decide when to add a test and make sure the test returns true! 303 | * A Grader hasn't disappeared after its queue empties. You can add more tests and run again! (Only for synchronous tests.) 304 | -------------------------------------------------------------------------------- /grader.js: -------------------------------------------------------------------------------- 1 | var Grader = (function() { 2 | 3 | // http://stackoverflow.com/questions/1068834/object-comparison-in-javascript?lq=1 4 | function deepCompare () { 5 | var i, l, leftChain, rightChain; 6 | 7 | function compare2Objects (x, y) { 8 | var p; 9 | 10 | // remember that NaN === NaN returns false 11 | // and isNaN(undefined) returns true 12 | if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') { 13 | return true; 14 | } 15 | 16 | // Compare primitives and functions. 17 | // Check if both arguments link to the same object. 18 | // Especially useful on step when comparing prototypes 19 | if (x === y) { 20 | return true; 21 | } 22 | 23 | // Works in case when functions are created in constructor. 24 | // Comparing dates is a common scenario. Another built-ins? 25 | // We can even handle functions passed across iframes 26 | if ((typeof x === 'function' && typeof y === 'function') || 27 | (x instanceof Date && y instanceof Date) || 28 | (x instanceof RegExp && y instanceof RegExp) || 29 | (x instanceof String && y instanceof String) || 30 | (x instanceof Number && y instanceof Number)) { 31 | return x.toString() === y.toString(); 32 | } 33 | 34 | // At last checking prototypes as good a we can 35 | if (!(x instanceof Object && y instanceof Object)) { 36 | return false; 37 | } 38 | 39 | if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) { 40 | return false; 41 | } 42 | 43 | if (x.constructor !== y.constructor) { 44 | return false; 45 | } 46 | 47 | if (x.prototype !== y.prototype) { 48 | return false; 49 | } 50 | 51 | // Check for infinitive linking loops 52 | if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) { 53 | return false; 54 | } 55 | 56 | // Quick checking of one object beeing a subset of another. 57 | // todo: cache the structure of arguments[0] for performance 58 | for (p in y) { 59 | if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { 60 | return false; 61 | } 62 | else if (typeof y[p] !== typeof x[p]) { 63 | return false; 64 | } 65 | } 66 | 67 | for (p in x) { 68 | if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { 69 | return false; 70 | } 71 | else if (typeof y[p] !== typeof x[p]) { 72 | return false; 73 | } 74 | 75 | switch (typeof (x[p])) { 76 | case 'object': 77 | case 'function': 78 | leftChain.push(x); 79 | rightChain.push(y); 80 | 81 | if (!compare2Objects (x[p], y[p])) { 82 | return false; 83 | } 84 | 85 | leftChain.pop(); 86 | rightChain.pop(); 87 | break; 88 | 89 | default: 90 | if (x[p] !== y[p]) { 91 | return false; 92 | } 93 | break; 94 | } 95 | } 96 | 97 | return true; 98 | } 99 | 100 | if (arguments.length < 1) { 101 | return true; //Die silently? Don't know how to handle such case, please help... 102 | // throw "Need two or more arguments to compare"; 103 | } 104 | 105 | for (i = 1, l = arguments.length; i < l; i++) { 106 | 107 | leftChain = []; //Todo: this can be cached 108 | rightChain = []; 109 | 110 | if (!compare2Objects(arguments[0], arguments[i])) { 111 | return false; 112 | } 113 | } 114 | return true; 115 | } 116 | 117 | function Queue (grader) { 118 | this.grader = grader; 119 | this.gradingSteps = []; 120 | this.flushing = false; 121 | this.alwaysGo = false; 122 | }; 123 | 124 | Queue.prototype = { 125 | add: function(callback, messages, keepGoing) { 126 | if (keepGoing !== false) { 127 | keepGoing = true; 128 | } 129 | 130 | if (!callback) { 131 | throw new Error("Every test added to the queue must have a valid function."); 132 | } 133 | 134 | this.gradingSteps.push({ 135 | callback: callback, 136 | isCorrect: false, 137 | wrongMessage: messages.wrongMessage || null, 138 | comment: messages.comment || null, 139 | category: messages.category || null, 140 | keepGoing: keepGoing 141 | }); 142 | }, 143 | 144 | _flush: function() { 145 | if (!this.flushing) { 146 | this.flushing = true; 147 | } 148 | this.step(); 149 | }, 150 | 151 | clear: function() { 152 | this.flushing = false; 153 | this.gradingSteps = []; 154 | this.grader.endTests(); 155 | }, 156 | 157 | step: function() { 158 | var self = this; 159 | if (this.gradingSteps.length === 0) { 160 | this.clear(); 161 | } 162 | 163 | function executeInPromise (fn) { 164 | return new Promise(function(resolve, reject) { 165 | if (fn) { 166 | try { 167 | var result = fn(); 168 | } catch (e) { 169 | self.clear(); 170 | console.log(e); 171 | } 172 | } 173 | resolve(result); 174 | }); 175 | }; 176 | 177 | function takeNextStep (test, result) { 178 | test.isCorrect = result; 179 | 180 | self.registerResults(test); 181 | 182 | if (test.isCorrect || test.keepGoing || self.alwaysGo) { 183 | self.step(); 184 | } else { 185 | self.clear(); 186 | } 187 | }; 188 | 189 | if (this.flushing) { 190 | var test = this.gradingSteps.shift(); 191 | 192 | if (this.grader.async) { 193 | executeInPromise(test.callback).then(function(resolve) { 194 | takeNextStep(test, resolve); 195 | }); 196 | } else if (!this.grader.async) { 197 | try { 198 | var result = test.callback(); 199 | } catch (e) { 200 | console.log(e); 201 | throw new Error(); 202 | } 203 | takeNextStep(test, result); 204 | } 205 | 206 | } 207 | }, 208 | 209 | registerResults: function(test) { 210 | this.grader.registerResults(test); 211 | } 212 | }; 213 | 214 | function Grader (type, categoryMessages) { 215 | var self = this; 216 | this.specificFeedback = []; 217 | this.comments = []; 218 | this.isCorrect = false; 219 | this.correctHasChanged = false; 220 | this.queue = new Queue(self); 221 | this.async = false; 222 | this.categoryMessages = null; 223 | this.generalFeedback = []; 224 | this.onresult = function() {}; 225 | 226 | for (n in arguments) { 227 | switch (typeof arguments[n]) { 228 | case 'string': 229 | if (arguments[n] === 'async') { 230 | this.async = true; 231 | } else if (arguments[n] === 'sync') { 232 | this.async = false; 233 | } else { 234 | throw new Error("Invalid type argument in Grader constructor"); 235 | } 236 | break; 237 | case 'object': 238 | this.categoryMessages = arguments[n]; 239 | break; 240 | default: 241 | throw new TypeError("Invalid argument in Grader constructor"); 242 | break; 243 | } 244 | } 245 | }; 246 | 247 | Object.defineProperties(Grader.prototype, { 248 | // sometimes used instead of isCorrect 249 | is_correct: { 250 | get: function() { 251 | return this.isCorrect; 252 | } 253 | } 254 | }); 255 | 256 | Grader.prototype = { 257 | addTest: function(callback, messages, keepGoing) { 258 | this.queue.add(callback, messages, keepGoing); 259 | }, 260 | 261 | runTests: function(options) { 262 | if (options) { 263 | this.queue.alwaysGo = options.ignoreCheckpoints || false; 264 | } 265 | this.queue._flush(); 266 | }, 267 | 268 | endTests: function() { 269 | if (this.queue.flushing) { 270 | this.queue.clear(); 271 | } else { 272 | var results = this.gatherResults(); 273 | this.onresult(results); 274 | } 275 | }, 276 | 277 | registerResults: function(test) { 278 | this.generateSpecificFeedback(test); 279 | this.generateGeneralFeedback(test); 280 | this.setCorrect(test); 281 | }, 282 | 283 | generateSpecificFeedback: function(test) { 284 | if (!test.isCorrect && test.wrongMessage) { 285 | this.addSpecificFeedback(test.wrongMessage); 286 | } else if (test.isCorrect && test.comment) { 287 | this.addComment(test.comment) 288 | } 289 | }, 290 | 291 | generateGeneralFeedback: function(test) { 292 | if (!test.isCorrect && test.category) { 293 | if (this.generalFeedback.indexOf(this.categoryMessages[test.category]) === -1) { 294 | this.generalFeedback.push(this.categoryMessages[test.category]); 295 | } 296 | } 297 | }, 298 | 299 | setCorrect: function(test) { 300 | if (this.correctHasChanged) { 301 | this.isCorrect = this.isCorrect && test.isCorrect; 302 | } else { 303 | this.correctHasChanged = true; 304 | this.isCorrect = test.isCorrect; 305 | } 306 | }, 307 | 308 | addSpecificFeedback: function(feedback) { 309 | this.specificFeedback.push(feedback); 310 | }, 311 | 312 | addComment: function(feedback) { 313 | this.comments.push(feedback); 314 | }, 315 | 316 | gatherResults: function() { 317 | var self = this; 318 | return { 319 | isCorrect: self.isCorrect, 320 | testFeedback: self.specificFeedback.concat(self.generalFeedback), 321 | testComments: self.comments 322 | } 323 | }, 324 | 325 | getFormattedWrongMessages: function(separator) { 326 | var allMessages, message; 327 | 328 | allMessages = this.specificFeedback.concat(this.generalFeedback); 329 | message = allMessages.join(separator); 330 | 331 | return message; 332 | }, 333 | 334 | getFormattedComments: function(separator) { 335 | return this.comments.join(separator); 336 | }, 337 | 338 | isType: function(value, expectedType) { 339 | var isCorrect = false; 340 | 341 | if (typeof value !== expectedType) { 342 | 343 | if (typeof value === 'function') { 344 | value = value.name; 345 | }; 346 | 347 | isCorrect = false; 348 | } else if (typeof value === expectedType){ 349 | isCorrect = true; 350 | } 351 | return isCorrect; 352 | }, 353 | 354 | isInstance: function(value, expectedInstance) { 355 | var isCorrect = false; 356 | 357 | if (value instanceof expectedInstance !== true) { 358 | 359 | isCorrect = false; 360 | } else if (value instanceof expectedInstance === true){ 361 | isCorrect = true; 362 | } 363 | return isCorrect; 364 | }, 365 | 366 | isValue: function(value1, value2) { 367 | var isCorrect = false; 368 | 369 | if (!deepCompare(value1, value2)) { 370 | isCorrect = false; 371 | } else if (deepCompare(value1, value2)) { 372 | isCorrect = true; 373 | } 374 | return isCorrect; 375 | }, 376 | 377 | isInRange: function(value, lower, upper) { 378 | var isCorrect = false; 379 | 380 | if (typeof value !== 'number' || isNaN(value)) { 381 | isCorrect = false 382 | } else if (value > upper || value < lower) { 383 | isCorrect = false; 384 | 385 | } else if (value < upper || value > lower) { 386 | isCorrect = true; 387 | } 388 | return isCorrect; 389 | }, 390 | 391 | isSet: function(value) { 392 | var isCorrect = false; 393 | 394 | if (value === undefined) { 395 | isCorrect = false; 396 | 397 | } else { 398 | isCorrect = true; 399 | } 400 | return isCorrect; 401 | }, 402 | 403 | isjQuery: function(elem) { 404 | // could use obj.jquery, which will only return true if it is a jquery object 405 | var isjQ = false; 406 | if (elem instanceof $) { 407 | isjQ = true; 408 | } 409 | return isjQ; 410 | }, 411 | 412 | hasCorrectTag: function(elem, tag) { 413 | if (!this.isjQuery(elem)) { 414 | elem = $(elem); 415 | } 416 | var hasTag = false; 417 | if (elem.is(tag)) { 418 | hasTag = true; 419 | } 420 | return hasTag; 421 | }, 422 | 423 | hasCorrectClass: function(elem, className) { 424 | if (!this.isjQuery(elem)) { 425 | elem = $(elem); 426 | } 427 | var hasClass = false; 428 | if (elem.hasClass(className)) { 429 | hasClass = true; 430 | } 431 | return hasClass; 432 | }, 433 | 434 | hasCorrectId: function(elem, id) { 435 | if (!this.isjQuery(elem)) { 436 | elem = $(elem); 437 | } 438 | if (elem.is('#' + id)) return true; 439 | return false; 440 | }, 441 | 442 | hasCorrectText: function(elem, text) { 443 | if (!this.isjQuery(elem)) { 444 | elem = $(elem); 445 | } 446 | var hasText = false; 447 | var re = new RegExp(text); 448 | if (elem.text().match(re)) { 449 | hasText = true; 450 | } 451 | return hasText; 452 | }, 453 | 454 | hasAttr: function(elem, attrName, correctAttr) { 455 | var isCorrect = false; 456 | if (!this.isjQuery(elem)) { 457 | elem = $(elem); 458 | } 459 | if (correctAttr && elem.attr(attrName) === correctAttr) { 460 | isCorrect = true; 461 | } else if (!correctAttr && elem.attr(attrName)) { 462 | isCorrect = true; 463 | } 464 | return isCorrect; 465 | }, 466 | 467 | hasCorrectLength: function(elems, _length) { 468 | if (!this.isjQuery(elems)) { 469 | elems = $(elems); 470 | } 471 | var correctLength = false; 472 | var cLength = elems.length; 473 | if (cLength === _length) { 474 | correctLength = true; 475 | } 476 | return correctLength; 477 | }, 478 | 479 | isCorrectElem: function(elem, correctElem) { 480 | if (!this.isjQuery(elem)) { 481 | elem = $(elem); 482 | } 483 | var is = false; 484 | if (elem.is(correctElem)) { 485 | is = true; 486 | } 487 | return is; 488 | }, 489 | 490 | isCorrectCollection: function(collection, correctCollection) { 491 | if (!this.isjQuery(elem)) { 492 | elem = $(elem); 493 | } 494 | var is = false; 495 | if (collection.is(correctCollection)) { 496 | is = true; 497 | } 498 | return is; 499 | }, 500 | 501 | hasCorrectStyle: function(elem, cssProperty, _correctStyle) { 502 | if (!this.isjQuery(elem)) { 503 | elem = $(elem); 504 | } 505 | var hasCorrectStyle = false; 506 | /* if one style is passed, convert to array */ 507 | if (typeof _correctStyle === 'string') { 508 | _correctStyle = [_correctStyle]; 509 | } 510 | var currentStyle = elem.css(cssProperty); 511 | for (var i = 0; i < _correctStyle.length; i++) { 512 | if (currentStyle === _correctStyle[i]) { 513 | hasCorrectStyle = true; 514 | } 515 | } 516 | return hasCorrectStyle; 517 | }, 518 | 519 | propertyIsLessThan : function (elem, cssProperty, value) { 520 | if (!this.isjQuery(elem)) { 521 | elem = $(elem); 522 | } 523 | var is = false; 524 | if (elem.css(cssProperty) < value) { 525 | is = true; 526 | } 527 | return is; 528 | }, 529 | 530 | propertyIsGreaterThan : function (elem, cssProperty, value) { 531 | if (!this.isjQuery(elem)) { 532 | elem = $(elem); 533 | } 534 | var is = false; 535 | if (elem.css(cssProperty) > value) { 536 | is = true; 537 | } 538 | return is; 539 | }, 540 | 541 | doesExistInParent: function (elem, parentElem) { 542 | if (!this.isjQuery(elem)) { 543 | elem = $(elem); 544 | } 545 | if (!this.isjQuery(parentElem)) { 546 | parentElem = $(parentElem); 547 | } 548 | var inParent = false; 549 | if (parentElem.find(elem).length > 0) { 550 | inParent = true; 551 | } 552 | return inParent; 553 | }, 554 | 555 | elemDoesExist: function(elem) { 556 | if (!this.isjQuery(elem)) { 557 | elem = $(elem); 558 | } 559 | var exists = false; 560 | if (elem.length > 0) { 561 | exists = true; 562 | } 563 | return exists; 564 | }, 565 | 566 | areSiblings: function(elem1, elem2) { 567 | if (!this.isjQuery(elem1)) { 568 | elem1 = $(elem1); 569 | } 570 | if (!this.isjQuery(elem2)) { 571 | elem2 = $(elem2); 572 | } 573 | var siblingLove = false; 574 | if (elem1.siblings(elem2).length > 0) { 575 | siblingLove = true; 576 | } 577 | return siblingLove; 578 | }, 579 | 580 | isImmediateChild: function(elem, parentElem) { 581 | var isCorrect = false; 582 | if (this.isjQuery(elem)) { 583 | throw new Error("elem needs to be a string for Grader.isImmediateChild()"); 584 | } 585 | if (!this.isjQuery(parentElem)) { 586 | parentElem = $(parentElem); 587 | } 588 | if (parentElem.children(elem).length > 0) { 589 | isCorrect = true; 590 | } 591 | return isCorrect; 592 | }, 593 | 594 | hasParent: function(elem, parentElem) { 595 | var isCorrect = false; 596 | if (this.isjQuery(parentElem)) { 597 | throw new Error("parentElem needs to be a string for Grader.hasParent()"); 598 | } 599 | if (!this.isjQuery(elem)) { 600 | elem = $(elem); 601 | } 602 | if (elem.closest(parentElem).length > 0) { 603 | isCorrect = true; 604 | } 605 | return isCorrect; 606 | }, 607 | 608 | sendResultsToExecutor: function(o) { 609 | var output = JSON.stringify(o); 610 | console.info("UDACITY_RESULT:" + output); 611 | } 612 | } 613 | return Grader; 614 | })(); 615 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |I'm just doing this so I can host the file.
23 | 24 | 29 | 30 |