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
28window.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
75window.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