1 /** 2 * Copyright (c) 2012 The Chromium Authors. All rights reserved. 3 * Use of this source code is governed by a BSD-style license that can be 4 * found in the LICENSE file. 5 * 6 * The utility class defined in this file allow calculator tests to be written 7 * more succinctly. 8 * 9 * Tests that would be written with QUnit like this: 10 * 11 * test('Two Plus Two', function() { 12 * var mock = window.mockView.create(); 13 * var controller = new Controller(new Model(8), mock); 14 * deepEqual(mock.testButton('2'), [null, null, '2'], '2'); 15 * deepEqual(mock.testButton('+'), ['2', '+', null], '+'); 16 * deepEqual(mock.testButton('2'), ['2', '+', '2'], '2'); 17 * deepEqual(mock.testButton('='), ['4', '=', null], '='); 18 * }); 19 * 20 * can instead be written as: 21 * 22 * var run = calculatorTestRun.create(); 23 * run.test('Two Plus Two', '2 + 2 = [4]'); 24 */ 25 26 'use strict'; 27 28 window.mockView = { 29 30 create: function() { 31 var view = Object.create(this); 32 view.display = []; 33 return view; 34 }, 35 36 clearDisplay: function(values) { 37 this.display = []; 38 this.addValues(values); 39 }, 40 41 addResults: function(values) { 42 this.display.push([]); 43 this.addValues(values); 44 }, 45 46 addValues: function(values) { 47 this.display.push([ 48 values.accumulator || '', 49 values.operator || '', 50 values.operand || '' 51 ]); 52 }, 53 54 setValues: function(values) { 55 this.display.pop(); 56 this.addValues(values); 57 }, 58 59 getValues: function() { 60 var last = this.display[this.display.length - 1]; 61 return { 62 accumulator: last && last[0] || null, 63 operator: last && last[1] || null, 64 operand: last && last[2] || null 65 }; 66 }, 67 68 testButton: function(button) { 69 this.onButton.call(this, button); 70 return this.display; 71 } 72 73 }; 74 75 window.calculatorTestRun = { 76 77 BUTTONS: { 78 '0': 'zero', 79 '1': 'one', 80 '2': 'two', 81 '3': 'three', 82 '4': 'four', 83 '5': 'five', 84 '6': 'six', 85 '7': 'seven', 86 '8': 'eight', 87 '9': 'nine', 88 '.': 'point', 89 '+': 'add', 90 '-': 'subtract', 91 '*': 'multiply', 92 '/': 'divide', 93 '=': 'equals', 94 '~': 'negate', 95 'A': 'clear', 96 '<': 'back' 97 }, 98 99 NAMES: { 100 '~': '+ / -', 101 'A': 'AC', 102 '<': 'back', 103 }, 104 105 /** 106 * Returns an object representing a run of calculator tests. 107 */ 108 create: function() { 109 var run = Object.create(this); 110 run.tests = []; 111 run.success = true; 112 return run; 113 }, 114 115 /** 116 * Runs a test defined as either a sequence or a function. 117 */ 118 test: function(name, test) { 119 this.tests.push({name: name, steps: [], success: true}); 120 if (typeof test === 'string') 121 this.testSequence_(name, test); 122 else if (typeof test === 'function') 123 test.call(this, new Controller(new Model(8), window.mockView.create())); 124 else 125 this.fail(this.getDescription_('invalid test: ', test)); 126 }, 127 128 /** 129 * Log test failures to the console. 130 */ 131 log: function() { 132 var parts = ['\n\n', 0, ' tests passed, ', 0, ' failed.\n\n']; 133 if (!this.success) { 134 this.tests.forEach(function(test, index) { 135 var number = this.formatNumber_(index + 1, 2); 136 var prefix = test.success ? 'PASS: ' : 'FAIL: '; 137 parts[test.success ? 1 : 3] += 1; 138 parts.push(number, ') ', prefix, test.name, '\n'); 139 test.steps.forEach(function(step) { 140 var prefix = step.success ? 'PASS: ' : 'FAIL: '; 141 step.messages.forEach(function(message) { 142 parts.push(' ', prefix, message, '\n'); 143 prefix = ' '; 144 }); 145 }); 146 parts.push('\n'); 147 }.bind(this)); 148 console.log(parts.join('')); 149 } 150 }, 151 152 /** 153 * Verify that actual values after a test step match expectations. 154 */ 155 verify: function(expected, actual, message) { 156 if (this.areEqual_(expected, actual)) 157 this.succeed(message); 158 else 159 this.fail(message, expected, actual); 160 }, 161 162 /** 163 * Record a successful test step. 164 */ 165 succeed: function(message) { 166 var test = this.tests[this.tests.length - 1]; 167 test.steps.push({success: true, messages: [message]}); 168 }, 169 170 /** 171 * Fail the current test step. Expected and actual values are optional. 172 */ 173 fail: function(message, expected, actual) { 174 var test = this.tests[this.tests.length - 1]; 175 var failure = {success: false, messages: [message]}; 176 if (expected !== undefined) { 177 failure.messages.push(this.getDescription_('expected: ', expected)); 178 failure.messages.push(this.getDescription_('actual: ', actual)); 179 } 180 test.steps.push(failure); 181 test.success = false; 182 this.success = false; 183 }, 184 185 /** 186 * @private 187 * Tests how a new calculator controller handles a sequence of numbers, 188 * operations, and commands, verifying that the controller's view has expected 189 * values displayed after each input handled by the controller. 190 * 191 * Within the sequence string, expected values must be specified as arrays of 192 * the form described below. The strings '~', '<', and 'A' is interpreted as 193 * the commands '+ / -', 'back', and 'AC' respectively, and other strings are 194 * interpreted as the digits, periods, operations, and commands represented 195 * by those strings. 196 * 197 * Expected values are sequences of arrays of the following forms: 198 * 199 * [] 200 * [accumulator] 201 * [accumulator operator operand] 202 * [accumulator operator prefix suffix] 203 * 204 * where |accumulator|, |operand|, |prefix|, and |suffix| are numbers or 205 * underscores and |operator| is one of the operator characters or an 206 * underscore. The |operand|, |prefix|, and |suffix| numbers may contain 207 * leading zeros and embedded '=' characters which will be interpreted as 208 * described in the comments for the |testNumber_()| method above. Underscores 209 * represent values that are expected to be blank. '[]' arrays represent 210 * horizontal separators expected in the display. '[accumulator]' arrays 211 * adjust the last expected value array by setting only its accumulator value. 212 * If that value is already set they behave like '[accumulator _ accumulator]' 213 * arrays. 214 * 215 * Expected value array must be specified just after the sequence element 216 * which they are meant to test, like this: 217 * 218 * run.testSequence_(controller, '12 - 34 = [][-22 _ -22]') 219 * 220 * When expected values are not specified for an element, the following rules 221 * are applied to obtain best guesses for the expected values for that 222 * element's tests: 223 * 224 * - The initial expected values arrays are: 225 * 226 * [['', '', '']] 227 * 228 * - If the element being tested is a number, the expected operand value 229 * of the last expected value array is set and changed as described in the 230 * comments for the |testNumber_()| method above. 231 * 232 * - If the element being tested is the '+ / -' operation the expected 233 * values arrays will be changed as follows: 234 * 235 * [*, [x, y, '']] -> [*, [x, y, '']] 236 * [*, [x, y, z]] -> [*, [x, y, -z] 237 * [*, [x, y, z1, z2]] -> [*, [x, y, -z1z2] 238 * 239 * - If the element |e| being tested is the '+', '-', '*', or '/' operation 240 * the expected values will be changed as follows: 241 * 242 * [*, [x, y, '']] -> [*, ['', e, '']] 243 * [*, [x, y, z]] -> [*, [z, y, z], ['', e, '']] 244 * [*, [x, y, z1, z2]] -> [*, [z1z2, y, z1z2], ['', e, '']] 245 * 246 * - If the element being tested is the '=' command, the expected values 247 * will be changed as follows: 248 * 249 * [*, ['', '', '']] -> [*, [], ['0', '', '0']] 250 * [*, [x, y, '']] -> [*, [x, y, z], [], ['0', '', '0']] 251 * [*, [x, y, z]] -> [*, [x, y, z], [], [z, '', z]] 252 * [*, [x, y, z1, z2]] -> [*, [x, y, z], [], [z1z2, '', z1z2]] 253 * 254 * So for example this call: 255 * 256 * run.testSequence_('My Test', '12 + 34 - 56 = [][-10]') 257 * 258 * would yield the following tests: 259 * 260 * run.testInput_(controller, '1', [['', '', '1']]); 261 * run.testInput_(controller, '2', [['', '', '12']]); 262 * run.testInput_(controller, '+', [['12', '', '12'], ['', '+', '']]); 263 * run.testInput_(controller, '3', [['12', '', '12'], ['', '+', '3']]); 264 * run.testInput_(controller, '4', [..., ['', '+', '34']]); 265 * run.testInput_(controller, '-', [..., ['34', '', '34'], ['', '-', '']]); 266 * run.testInput_(controller, '2', [..., ['34', '', '34'], ['', '-', '2']]); 267 * run.testInput_(controller, '1', [..., ..., ['', '-', '21']]); 268 * run.testInput_(controller, '=', [[], [-10, '', -10]]); 269 */ 270 testSequence_: function(name, sequence) { 271 var controller = new Controller(new Model(8), window.mockView.create()); 272 var expected = [['', '', '']]; 273 var elements = this.parseSequence_(sequence); 274 for (var i = 0; i < elements.length; ++i) { 275 if (!Array.isArray(elements[i])) { // Skip over expected value arrays. 276 // Update and ajust expectations. 277 this.updatedExpectations_(expected, elements[i]); 278 if (Array.isArray(elements[i + 1] && elements[i + 1][0])) 279 expected = this.adjustExpectations_([], elements[i + 1], 0); 280 else 281 expected = this.adjustExpectations_(expected, elements, i + 1); 282 // Test. 283 if (elements[i].match(/^-?[\d.][\d.=]*$/)) 284 this.testNumber_(controller, elements[i], expected); 285 else 286 this.testInput_(controller, elements[i], expected); 287 }; 288 } 289 }, 290 291 /** @private */ 292 parseSequence_: function(sequence) { 293 // Define the patterns used below. 294 var ATOMS = /(-?[\d.][\d.=]*)|([+*/=~<CAE_-])/g; // number || command 295 var VALUES = /(\[[^\[\]]*\])/g; // expected values 296 // Massage the sequence into a JSON array string, so '2 + 2 = [4]' becomes: 297 sequence = sequence.replace(ATOMS, ',$1$2,'); // ',2, ,+, ,2, ,=, [,4,]' 298 sequence = sequence.replace(/\s+/g, ''); // ',2,,+,,2,,=,[,4,]' 299 sequence = sequence.replace(VALUES, ',$1,'); // ',2,,+,,2,,=,,[,4,],' 300 sequence = sequence.replace(/,,+/g, ','); // ',2,+,2,=,[,4,],' 301 sequence = sequence.replace(/\[,/g, '['); // ',2,+,2,=,[4,],' 302 sequence = sequence.replace(/,\]/g, ']'); // ',2,+,2,=,[4],' 303 sequence = sequence.replace(/(^,)|(,$)/g, ''); // '2,+,2,=,[4]' 304 sequence = sequence.replace(ATOMS, '"$1$2"'); // '"2","+","2","=",["4"]' 305 sequence = sequence.replace(/"_"/g, '""'); // '"2","+","2","=",["4"]' 306 // Fix some cases handled incorrectly by the massaging above, like the 307 // original sequences '[_ _ 0=]' and '[-1]', which would have become 308 // '["","","0","="]]' and '["-","1"]' respectively and would need to be 309 // fixed to '["","","0="]]' and '["-1"]'respectively. 310 sequence.replace(VALUES, function(match) { 311 return match.replace(/(","=)|(=",")/g, '=').replace(/-","/g, '-'); 312 }); 313 // Return an array created from the resulting JSON string. 314 return JSON.parse('[' + sequence + ']'); 315 }, 316 317 /** @private */ 318 updatedExpectations_: function(expected, element) { 319 var last = expected[expected.length - 1]; 320 var empty = (last && !last[0] && !last[1] && !last[2] && !last[3]); 321 var operand = last && last.slice(2).join(''); 322 var operation = element.match(/[+*/-]/); 323 var equals = (element === '='); 324 var negate = (element === '~'); 325 if (operation && !operand) 326 expected.splice(-1, 1, ['', element, '']); 327 else if (operation) 328 expected.splice(-1, 1, [operand, last[1], operand], ['', element, '']); 329 else if (equals && empty) 330 expected.splice(-1, 1, [], [operand || '0', '', operand || '0']); 331 else if (equals) 332 expected.push([], [operand || '0', '', operand || '0']); 333 else if (negate && operand) 334 expected[expected.length - 1].splice(2, 2, '-' + operand); 335 }, 336 337 /** @private */ 338 adjustExpectations_: function(expectations, adjustments, start) { 339 var replace = !expectations.length; 340 var adjustment, expectation; 341 for (var i = 0; Array.isArray(adjustments[start + i]); ++i) { 342 adjustment = adjustments[start + i]; 343 expectation = expectations[expectations.length - 1]; 344 if (adjustments[start + i].length != 1) { 345 expectations.splice(-i - 1, replace ? 0 : 1); 346 expectations.push(adjustments[start + i]); 347 } else if (i || !expectation || !expectation.length || expectation[0]) { 348 expectations.splice(-i - 1, replace ? 0 : 1); 349 expectations.push([adjustment[0], '', adjustment[0]]); 350 } else { 351 expectations[expectations.length - i - 2][0] = adjustment[0]; 352 } 353 } 354 return expectations; 355 }, 356 357 /** 358 * @private 359 * Tests how a calculator controller handles a sequence of digits and periods 360 * representing a number. During the test, the expected operand values are 361 * updated before each digit and period of the input according to these rules: 362 * 363 * - If the last of the passed in expected values arrays has an expected 364 * accumulator value, add an empty expected values array and proceed 365 * according to the rules below with this new array. 366 * 367 * - If the last of the passed in expected values arrays has no expected 368 * operand value and no expected operand prefix and suffix values, the 369 * expected operand used for the tests will start with the first digit or 370 * period of the numeric sequence and the following digits and periods of 371 * that sequence will be appended to that expected operand before each of 372 * the following digits and periods in the test. 373 * 374 * - If the last of the passed in expected values arrays has a single 375 * expected operand value instead of operand prefix and suffix values, the 376 * expected operand used for the tests will start with the first character 377 * of that operand value and one additional character of that value will 378 * be added to the expected operand before each of the following digits 379 * and periods in the tests. 380 * 381 * - If the last of the passed in expected values arrays has operand prefix 382 * and suffix values instead of an operand value, the expected operand 383 * used for the tests will start with the prefix value and the first 384 * character of the suffix value, and one character of that suffix value 385 * will be added to the expected operand before each of the following 386 * digits and periods in the tests. 387 * 388 * - In all of these cases, leading zeros and occurrences of the '=' 389 * character in the expected operand value will be ignored. 390 * 391 * For example the sequence of calls: 392 * 393 * run.testNumber_(controller, '00', [[x, y, '0=']]) 394 * run.testNumber_(controller, '1.2.3', [[x, y, '1.2=3']]) 395 * run.testNumber_(controller, '45', [[x, y, '1.23', '45']]) 396 * 397 * would yield the following tests: 398 * 399 * run.testInput_(controller, '0', [[x, y, '0']]); 400 * run.testInput_(controller, '0', [[x, y, '0']]); 401 * run.testInput_(controller, '1', [[x, y, '1']]); 402 * run.testInput_(controller, '.', [[x, y, '1.']]); 403 * run.testInput_(controller, '2', [[x, y, '1.2']]); 404 * run.testInput_(controller, '.', [[x, y, '1.2']]); 405 * run.testInput_(controller, '3', [[x, y, '1.23']]); 406 * run.testInput_(controller, '4', [[x, y, '1.234']]); 407 * run.testInput_(controller, '5', [[x, y, '1.2345']]); 408 * 409 * It would also changes the expected value arrays to the following: 410 * 411 * [[x, y, '1.2345']] 412 */ 413 testNumber_: function(controller, number, expected) { 414 var last = expected[expected.length - 1]; 415 var prefix = (last && !last[0] && last.length > 3 && last[2]) || ''; 416 var suffix = (last && !last[0] && last[last.length - 1]) || number; 417 var append = (last && !last[0]) ? ['', last[1], ''] : ['', '', '']; 418 var start = (last && !last[0]) ? -1 : expected.length; 419 var count = (last && !last[0]) ? 1 : 0; 420 expected.splice(start, count, append); 421 for (var i = 0; i < number.length; ++i) { 422 append[2] = prefix + suffix.slice(0, i + 1); 423 append[2] = append[2].replace(/^0+([0-9])/, '$1').replace(/=/g, ''); 424 this.testInput_(controller, number[i], expected); 425 } 426 }, 427 428 /** 429 * @private 430 * Tests how a calculator controller handles a single element of input, 431 * logging the state of the controller before and after the test. 432 */ 433 testInput_: function(controller, input, expected) { 434 var prefix = ['"', this.NAMES[input] || input, '": ']; 435 var before = this.addDescription_(prefix, controller, ' => '); 436 var display = controller.view.testButton(this.BUTTONS[input]); 437 var actual = display.slice(-expected.length); 438 this.verify(expected, actual, this.getDescription_(before, controller)); 439 }, 440 441 /** @private */ 442 areEqual_: function(x, y) { 443 return Array.isArray(x) ? this.areArraysEqual_(x, y) : (x == y); 444 }, 445 446 /** @private */ 447 areArraysEqual_: function(a, b) { 448 return Array.isArray(a) && 449 Array.isArray(b) && 450 a.length === b.length && 451 a.every(function(element, i) { 452 return this.areEqual_(a[i], b[i]); 453 }, this); 454 }, 455 456 /** @private */ 457 getDescription_: function(prefix, object, suffix) { 458 var strings = Array.isArray(prefix) ? prefix : prefix ? [prefix] : []; 459 return this.addDescription_(strings, object, suffix).join(''); 460 }, 461 462 /** @private */ 463 addDescription_: function(prefix, object, suffix) { 464 var strings = Array.isArray(prefix) ? prefix : prefix ? [prefix] : []; 465 if (Array.isArray(object)) { 466 strings.push('[', ''); 467 object.forEach(function(element) { 468 this.addDescription_(strings, element, ', '); 469 }, this); 470 strings.pop(); // Pops the last ', ', or pops '' for empty arrays. 471 strings.push(']'); 472 } else if (typeof object === 'number') { 473 strings.push('#'); 474 strings.push(String(object)); 475 } else if (typeof object === 'string') { 476 strings.push('"'); 477 strings.push(object); 478 strings.push('"'); 479 } else if (object instanceof Controller) { 480 strings.push('('); 481 this.addDescription_(strings, object.model.accumulator, ' '); 482 this.addDescription_(strings, object.model.operator, ' '); 483 this.addDescription_(strings, object.model.operand, ' | '); 484 this.addDescription_(strings, object.model.defaults.operator, ' '); 485 this.addDescription_(strings, object.model.defaults.operand, ')'); 486 } else { 487 strings.push(String(object)); 488 } 489 strings.push(suffix || ''); 490 return strings; 491 }, 492 493 /** @private */ 494 formatNumber_: function(number, digits) { 495 var string = String(number); 496 var array = Array(Math.max(digits - string.length, 0) + 1); 497 array[array.length - 1] = string; 498 return array.join('0'); 499 } 500 501 }; 502