| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| |
| /** |
| * @fileoverview Test utilities for JavaScript tests of the local NTP. |
| */ |
| |
| |
| /** |
| * Object for individual "module" objects, that then contain tests. |
| */ |
| var test = {}; |
| |
| |
| /** |
| * Shortcut for document.getElementById. |
| * @param {string} id of the element. |
| * @return {HTMLElement} with the id. |
| */ |
| function $(id) { |
| return document.getElementById(id); |
| } |
| |
| |
| /** |
| * Sets up the page for the next test case. Recreates a part of |
| * the default local NTP DOM (test specific, based on the template). |
| * @param {!string} templateId ID of the element to clone into the body element. |
| */ |
| function setUpPage(templateId) { |
| // First, clear up the DOM and state left over from any previous test case. |
| document.body.innerHTML = ''; |
| // The NTP stores some state such as fakebox focus in the body's classList. |
| document.body.classList = ''; |
| |
| document.body.appendChild($(templateId).content.cloneNode(true)); |
| } |
| |
| |
| /** |
| * Aborts a test if a condition is not met. |
| * @param {boolean} condition The condition that must be true. |
| * @param {string=} opt_message A message to log if the condition is not met. |
| */ |
| function assert(condition, opt_message) { |
| if (!condition) { |
| throw new Error(opt_message || 'Assertion failed'); |
| } |
| } |
| |
| |
| /** |
| * Aborts a test if |got| !== true. |
| * @param {boolean} got The boolean that must be true. |
| * @param {string=} opt_message A message to log if the condition is not met. |
| */ |
| function assertTrue(got, opt_message) { |
| if (got !== true) { |
| throw new Error(opt_message || `Expected: 'true', got: 'false'`); |
| } |
| } |
| |
| |
| /** |
| * Aborts a test if |got| !== false. |
| * @param {boolean} got The boolean that must be false. |
| * @param {string=} opt_message A message to log if the condition is not met. |
| */ |
| function assertFalse(got, opt_message) { |
| if (got !== false) { |
| throw new Error(opt_message || `Expected: 'false', got: 'true'`); |
| } |
| } |
| |
| |
| /** |
| * Aborts a test if |expected| !== |got|. |
| * @param {object} expected The expected value. |
| * @param {object} got The obtained value. |
| * @param {string=} opt_message A message to log if the paremeters |
| * are not equal. |
| */ |
| function assertEquals(expected, got, opt_message) { |
| if (expected !== got) { |
| throw new Error(opt_message || `Expected: '${expected}', got: '${got}'`); |
| } |
| } |
| |
| |
| /** |
| * Aborts a test if the |throws| function does not throw an error, |
| * or the error message does not contain |expected|. |
| * @param {!string} expected The expected error message substring. |
| * @param {!Function} throws The function that should throw an error with |
| * a message containing |expected| as a substring. |
| * @param {string=} opt_message A message to log if this assertion fails. |
| */ |
| function assertThrows(expected, throws, opt_message) { |
| try { |
| throws.call(); |
| } catch (err) { |
| if (err.message.includes(expected)) { |
| return; |
| } |
| throw new Error(opt_message || `Expected: '${expected}', got: '${err}'`); |
| } |
| throw new Error(opt_message || 'Got no error.'); |
| } |
| |
| |
| /** |
| * Calls |runTest()| on all simple tests within the |test[moduleName]| object |
| * (any function in that object whose name starts with 'test'). |
| * @param {!string} moduleName Name of the module to test (used as |
| * an index into |test|). |
| * @return {boolean} True if all tests pass and false otherwise. |
| */ |
| function runSimpleTests(moduleName) { |
| let totalTests = 0; |
| let passedTests = 0; |
| const testModule = window.test[moduleName]; |
| if (!testModule) { |
| throw new Error('Failed to find tests for module: ' + moduleName); |
| } |
| |
| for (let testName in testModule) { |
| if (/^test.+/.test(testName) && typeof testModule[testName] == 'function') { |
| totalTests++; |
| try { |
| if (runTest(testModule, testName)) { |
| passedTests++; |
| } |
| } catch (err) { |
| console.log('Error occurred while running tests, aborting.'); |
| return false; |
| } |
| } |
| } |
| window.console.log(`Passed tests: ${passedTests}/${totalTests}`); |
| return passedTests == totalTests; |
| } |
| |
| |
| /** |
| * Sets up the environment using the |testModule.setUp()| function |
| * if it exists, and calls the function under test, |testModule[testName]|. |
| * The setup function is assumed to be provided by the browser |
| * test script. In case setup fails, the error is logged and another error |
| * is raised, which will abort the test procedure in |runSimpleTests()|. |
| * @param {!object} testModule The object that contains tests for a module |
| * (a property of |test|). |
| * @param {!string} testName The name of the test function to run. |
| * @return {boolean} True iff the test function succeeds (does not |
| * throw an error). |
| */ |
| function runTest(testModule, testName) { |
| try { |
| if (typeof testModule.setUp === 'function') { |
| testModule.setUp(); |
| } |
| } catch (err) { |
| window.console.log(`Setup for '${testName}' failed: ${err}`); |
| window.console.log('Stacktrace:\n' + err.stack); |
| throw new Error('Setup failed, aborting tests.'); |
| } |
| |
| // Run the test function. |
| try { |
| testModule[testName].call(testModule); |
| } catch (err) { |
| window.console.log(`Test '${testName}' failed: ${err}`); |
| window.console.log('Stacktrace:\n' + err.stack); |
| return false; |
| } |
| return true; |
| } |
| |
| |
| /** |
| * Checks whether a given HTMLElement exists and is visible. |
| * @param {HTMLElement|undefined} elem An HTMLElement. |
| * @return {boolean} True if the element exists and is visible. |
| */ |
| function elementIsVisible(elem) { |
| return elem && elem.offsetWidth > 0 && elem.offsetHeight > 0 && |
| window.getComputedStyle(elem).visibility != 'hidden' && |
| window.getComputedStyle(elem).opacity > 0; |
| } |
| |
| |
| /** |
| * Creates and initializes a LocalNTP object. |
| * @param {boolean} isGooglePage Whether to make it a Google-branded NTP. |
| * @param {?boolean=} removeFakebox Whether the fakebox should be hidden. |
| * @return {!Object} The LocalNTP object. |
| */ |
| function initLocalNTP(isGooglePage, removeFakebox = false) { |
| configData.isGooglePage = isGooglePage; |
| configData.removeFakebox = removeFakebox; |
| var localNTP = LocalNTP(); |
| localNTP.disableIframesAndVoiceSearchForTesting(); |
| localNTP.init(); |
| return localNTP; |
| } |
| |
| |
| // ***************************** HELPER CLASSES ***************************** |
| // Helper classes used for mocking in tests. |
| |
| |
| /** |
| * A property replacer utility class. Useful for substituting real functions |
| * and properties with mocks and then reverting the changes. Inspired by |
| * Closure's PropertyReplacer class. |
| * @constructor |
| */ |
| function Replacer() { |
| /** |
| * A dictionary of temporary objects that contain a reference to |
| * the property's parent object (|parent|), and the property's |
| * original value (|value|). |
| */ |
| this.old_ = {}; |
| } |
| |
| |
| /** |
| * Replaces the property |propertyName| of the object |parent| with the |
| * |newValue|. Stores the old value of the property to enable resetting |
| * in |reset()|. |
| * @param {!object} parent The object containg the property |propertyName|. |
| * @param {!string} propertyName The name of the property. |
| * @param {object} newValue The new value of the property. |
| */ |
| Replacer.prototype.replace = function(parent, propertyName, newValue) { |
| if (!(propertyName in parent)) { |
| throw new Error( |
| `Cannot replace missing property "${propertyName}" in parent.`); |
| } |
| this.old_[propertyName] = {}; |
| this.old_[propertyName].value = parent[propertyName]; |
| this.old_[propertyName].parent = parent; |
| parent[propertyName] = newValue; |
| }; |
| |
| |
| /** |
| * Resets every property that was overriden by |replace()| to its original |
| * value. |
| */ |
| Replacer.prototype.reset = function() { |
| Object.entries(this.old_).forEach(([propertyName, saved]) => { |
| saved.parent[propertyName] = saved.value; |
| }); |
| this.old_ = {}; |
| }; |
| |
| |
| /** |
| * Utility for testing code that uses |setTimeout()| and |clearTimeout()|. |
| * Inspired by Closure's MockClock class. |
| * @constructor |
| */ |
| function MockClock() { |
| /** |
| * The current time, in milliseconds. |
| */ |
| this.time = 0; |
| |
| |
| /** |
| * Records the timeouts that have been set and not yet executed, as an array |
| * of {callback, activationTime, id} objects, ordered by activationTime. |
| */ |
| this.pendingTimeouts = []; |
| |
| |
| /** |
| * An internal counter for assigning timeout ids. |
| */ |
| this.nextTimeoutId_ = 1; |
| |
| |
| /** |
| * Property replacer, used for replacing |setTimeout()| and |clearTimeout()|. |
| */ |
| this.stubs_ = new Replacer(); |
| } |
| |
| |
| /** |
| * Sets the current time that sbox.getTime returns. |
| * @param {!number} msec The time in milliseconds. |
| */ |
| MockClock.prototype.setTime = function(msec) { |
| assert(this.time <= msec); |
| this.time = msec; |
| }; |
| |
| |
| /** |
| * Advances the current time by the specified number of milliseconds. |
| * @param {!number} msec The length of time to advance the current time by. |
| */ |
| MockClock.prototype.advanceTime = function(msec) { |
| assert(msec >= 0); |
| this.time += msec; |
| }; |
| |
| |
| /** |
| * Restores the |setTimeout()| and |clearTimeout()| functions back to their |
| * original implementation. |
| */ |
| MockClock.prototype.reset = function() { |
| this.time = 0; |
| this.pendingTimeouts = []; |
| this.nextTimeoutId_ = 1; |
| this.stubs_.reset(); |
| }; |
| |
| |
| /** |
| * Checks whether a timeout with this id has been set. |
| * @param {!number} id The timeout id. |
| * @return True iff a timeout with this id has been set and the timeout |
| * has not yet fired. |
| */ |
| MockClock.prototype.isTimeoutSet = function(id) { |
| return 0 < id && id <= this.nextTimeoutId_ && |
| this.pendingTimeouts.map(t => t.id).includes(id); |
| }; |
| |
| |
| /** |
| * Substitutes the global |setTimeout()| and |clearTimeout()| functions |
| * with their mocked variants, until |reset()| is called. |
| */ |
| MockClock.prototype.install = function() { |
| this.stubs_.replace(window, 'clearTimeout', (id) => { |
| if (!id) { |
| return; |
| } |
| for (let i = 0; i < this.pendingTimeouts.length; ++i) { |
| if (this.pendingTimeouts[i].id == id) { |
| this.pendingTimeouts.splice(i, 1); |
| return; |
| } |
| } |
| }); |
| this.stubs_.replace(window, 'setTimeout', (callback, interval) => { |
| const timeoutId = this.nextTimeoutId_++; |
| this.pendingTimeouts.push({ |
| 'callback': callback, |
| 'activationTime': this.time + interval, |
| 'id': timeoutId |
| }); |
| this.pendingTimeouts.sort(function(a, b) { |
| return a.activationTime - b.activationTime; |
| }); |
| return timeoutId; |
| }); |
| }; |