Home | History | Annotate | Download | only in tests
      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