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.
6 * The utility class defined in this file allow calculator tests to be written
9 * Tests that would be written with QUnit like this:
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], '=');
20 * can instead be written as:
22 * var run = calculatorTestRun.create();
23 * run.test('Two Plus Two', '2 + 2 = [4]');
31 var view = Object.create(this);
36 clearDisplay: function(values) {
38 this.addValues(values);
41 addResults: function(values) {
42 this.display.push([]);
43 this.addValues(values);
46 addValues: function(values) {
48 values.accumulator || '',
49 values.operator || '',
54 setValues: function(values) {
56 this.addValues(values);
59 getValues: function() {
60 var last = this.display[this.display.length - 1];
62 accumulator: last && last[0] || null,
63 operator: last && last[1] || null,
64 operand: last && last[2] || null
68 testButton: function(button) {
69 this.onButton.call(this, button);
75 window.calculatorTestRun = {
106 * Returns an object representing a run of calculator tests.
109 var run = Object.create(this);
116 * Runs a test defined as either a sequence or a function.
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()));
125 this.fail(this.getDescription_('invalid test: ', test));
129 * Log test failures to the console.
132 var parts = ['\n\n', 0, ' tests passed, ', 0, ' failed.\n\n'];
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');
148 console.log(parts.join(''));
153 * Verify that actual values after a test step match expectations.
155 verify: function(expected, actual, message) {
156 if (this.areEqual_(expected, actual))
157 this.succeed(message);
159 this.fail(message, expected, actual);
163 * Record a successful test step.
165 succeed: function(message) {
166 var test = this.tests[this.tests.length - 1];
167 test.steps.push({success: true, messages: [message]});
171 * Fail the current test step. Expected and actual values are optional.
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));
180 test.steps.push(failure);
181 test.success = false;
182 this.success = false;
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.
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
197 * Expected values are sequences of arrays of the following forms:
201 * [accumulator operator operand]
202 * [accumulator operator prefix suffix]
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]'
215 * Expected value array must be specified just after the sequence element
216 * which they are meant to test, like this:
218 * run.testSequence_(controller, '12 - 34 = [][-22 _ -22]')
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
224 * - The initial expected values arrays are:
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.
232 * - If the element being tested is the '+ / -' operation the expected
233 * values arrays will be changed as follows:
235 * [*, [x, y, '']] -> [*, [x, y, '']]
236 * [*, [x, y, z]] -> [*, [x, y, -z]
237 * [*, [x, y, z1, z2]] -> [*, [x, y, -z1z2]
239 * - If the element |e| being tested is the '+', '-', '*', or '/' operation
240 * the expected values will be changed as follows:
242 * [*, [x, y, '']] -> [*, ['', e, '']]
243 * [*, [x, y, z]] -> [*, [z, y, z], ['', e, '']]
244 * [*, [x, y, z1, z2]] -> [*, [z1z2, y, z1z2], ['', e, '']]
246 * - If the element being tested is the '=' command, the expected values
247 * will be changed as follows:
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]]
254 * So for example this call:
256 * run.testSequence_('My Test', '12 + 34 - 56 = [][-10]')
258 * would yield the following tests:
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]]);
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);
281 expected = this.adjustExpectations_(expected, elements, i + 1);
283 if (elements[i].match(/^-?[\d.][\d.=]*$/))
284 this.testNumber_(controller, elements[i], expected);
286 this.testInput_(controller, elements[i], expected);
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, '-');
313 // Return an array created from the resulting JSON string.
314 return JSON.parse('[' + sequence + ']');
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, '']);
328 expected.splice(-1, 1, [operand, last[1], operand], ['', element, '']);
329 else if (equals && empty)
330 expected.splice(-1, 1, [], [operand || '0', '', operand || '0']);
332 expected.push([], [operand || '0', '', operand || '0']);
333 else if (negate && operand)
334 expected[expected.length - 1].splice(2, 2, '-' + operand);
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]]);
351 expectations[expectations.length - i - 2][0] = adjustment[0];
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:
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.
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.
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.
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.
388 * - In all of these cases, leading zeros and occurrences of the '='
389 * character in the expected operand value will be ignored.
391 * For example the sequence of calls:
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']])
397 * would yield the following tests:
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']]);
409 * It would also changes the expected value arrays to the following:
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);
430 * Tests how a calculator controller handles a single element of input,
431 * logging the state of the controller before and after the test.
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));
442 areEqual_: function(x, y) {
443 return Array.isArray(x) ? this.areArraysEqual_(x, y) : (x == y);
447 areArraysEqual_: function(a, b) {
448 return Array.isArray(a) &&
450 a.length === b.length &&
451 a.every(function(element, i) {
452 return this.areEqual_(a[i], b[i]);
457 getDescription_: function(prefix, object, suffix) {
458 var strings = Array.isArray(prefix) ? prefix : prefix ? [prefix] : [];
459 return this.addDescription_(strings, object, suffix).join('');
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, ', ');
470 strings.pop(); // Pops the last ', ', or pops '' for empty arrays.
472 } else if (typeof object === 'number') {
474 strings.push(String(object));
475 } else if (typeof object === 'string') {
477 strings.push(object);
479 } else if (object instanceof Controller) {
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, ')');
487 strings.push(String(object));
489 strings.push(suffix || '');
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');