// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


/**
 * @fileoverview Unit tests for Closure's base.js.
 */

goog.provide('goog.baseTest');

goog.setTestOnly('goog.baseTest');

goog.require('goog.Promise');
// Used to test dynamic loading works, see testRequire*
goog.require('goog.Timer');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.functions');
goog.require('goog.object');
goog.require('goog.test_module');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
goog.require('goog.userAgent');

var earlyTestModuleGet = goog.module.get('goog.test_module');

/**
 * @param {?} name
 * @return {?}
 */
function getFramedVars(name) {
  var w = window.frames[name];
  var doc = w.document;
  doc.open();
  doc.write(
      '<script>' +
      'var a = [0, 1, 2];' +
      'var o = {a: 0, b: 1};' +
      'var n = 42;' +
      'var b = true;' +
      'var s = "string";' +
      'var nv = null;' +
      'var u = undefined;' +
      'var fv = function(){};' +
      '</' +
      'script>');
  doc.close();
  return {
    'array': w.a,
    'object': w.o,
    'number': w.n,
    'boolean': w.b,
    'string': w.s,
    'functionVar': w.fv,
    'nullVar': w.nv,
    'undefinedVar': w.u
  };
}

var framedVars = getFramedVars('f1');
var framedVars2 = getFramedVars('f2');
// remove iframe
var iframeElement = document.getElementById('f2');
iframeElement.parentNode.removeChild(iframeElement);
var stubs = new goog.testing.PropertyReplacer();
var originalGoogBind = goog.bind;

function tearDown() {
  goog.setCssNameMapping(undefined);
  stubs.reset();
  goog.bind = originalGoogBind;
}

function testLibrary() {
  assertNotUndefined('\'goog\' not loaded', goog);
}

function testDefine() {
  goog.define('SOME_DEFINE', 123);  // overridden by 456
  assertEquals(SOME_DEFINE, 456);

  goog.define('SOME_OTHER_DEFINE', 123);  // not overridden
  assertEquals(SOME_OTHER_DEFINE, 123);

  // alias to avoid the being picked up by the deps scanner.
  var provide = goog.provide;
  provide('ns');

  goog.define('ns.SOME_DEFINE', 123);  // overridden by 456
  assertEquals(SOME_DEFINE, 456);

  goog.define('ns.SOME_OTHER_DEFINE', 123);  // not overridden
  assertEquals(SOME_OTHER_DEFINE, 123);
}

function testProvide() {
  // alias to avoid the being picked up by the deps scanner.
  var provide = goog.provide;

  provide('goog.test.name.space');
  assertNotUndefined('provide failed: goog.test', goog.test);
  assertNotUndefined('provide failed: goog.test.name', goog.test.name);
  assertNotUndefined(
      'provide failed: goog.test.name.space', goog.test.name.space);

  // ensure that providing 'goog.test.name' doesn't throw an exception
  provide('goog.test');
  provide('goog.test.name');
  delete goog.test;
}

// "watch" is a native member of Object.prototype on Firefox
// Ensure it can still be added as a namespace
function testProvideWatch() {
  // alias to avoid the being picked up by the deps scanner.
  var provide = goog.provide;

  provide('goog.yoddle.watch');
  assertNotUndefined('provide failed: goog.yoddle.watch', goog.yoddle.watch);
  delete goog.yoddle;
}

// Namespaces should not conflict with elements added to the window based on
// their id
function testConflictingSymbolAndId() {
  // Create a div with a given id
  var divElement = document.createElement('div');
  divElement.id = 'clashingname';
  document.body.appendChild(divElement);

  // The object at window.clashingname is the element with that id
  assertEquals(window.clashingname, divElement);

  // Export a symbol to a sub-namespace of that id
  var symbolObject = {};
  goog.exportSymbol('clashingname.symbolname', symbolObject);

  // The symbol has been added...
  assertEquals(window.clashingname.symbolname, symbolObject);

  // ...and has not affected the original div
  assertEquals(window.clashingname, divElement);
}

function testProvideStrictness() {
  // alias to avoid the being picked up by the deps scanner.
  var provide = goog.provide;

  provide('goog.xy');
  assertProvideFails('goog.xy');

  provide('goog.xy.z');
  assertProvideFails('goog.xy');

  window['goog']['xyz'] = 'Bob';
  assertProvideFails('goog.xyz');

  delete goog.xy;
  delete goog.xyz;
}

/** @param {?} namespace */
function assertProvideFails(namespace) {
  assertThrows(
      'goog.provide(' + namespace + ') should have failed',
      goog.partial(goog.provide, namespace));
}

function testIsProvided() {
  // alias to avoid the being picked up by the deps scanner.
  var provide = goog.provide;

  provide('goog.explicit');
  assertTrue(goog.isProvided_('goog.explicit'));
  provide('goog.implicit.explicit');
  assertFalse(goog.isProvided_('goog.implicit'));
  assertTrue(goog.isProvided_('goog.implicit.explicit'));
}

function testGlobalize() {
  var a = {a: 1, b: 2, c: 3};
  var b = {};
  goog.globalize(a, b);
  assertNotUndefined('Globalize to arbitrary object', b.a);
  assertNotUndefined('Globalize to arbitrary object', b.b);
  assertNotUndefined('Globalize to arbitrary object', b.c);
}

function testExportSymbol() {
  var date = new Date();

  // alias to avoid the being picked up by the deps scanner.
  var provide = goog.provide;

  assertTrue(typeof nodots == 'undefined');
  goog.exportSymbol('nodots', date);
  assertEquals(date, nodots);
  nodots = undefined;

  assertTrue(typeof gotcher == 'undefined');
  goog.exportSymbol('gotcher.dots.right.Here', date);
  assertEquals(date, gotcher.dots.right.Here);
  gotcher = undefined;

  provide('an.existing.path');
  assertNotNull(an.existing.path);
  goog.exportSymbol('an.existing.path', date);
  assertEquals(date, an.existing.path);
  an = undefined;

  var foo = {foo: 'foo'};
  var bar = {bar: 'bar'};
  var baz = {baz: 'baz'};
  goog.exportSymbol('one.two.three.Four', foo);
  goog.exportSymbol('one.two.three.five', bar);
  goog.exportSymbol('one.two.six', baz);
  assertEquals(foo, one.two.three.Four);
  assertEquals(bar, one.two.three.five);
  assertEquals(baz, one.two.six);

  var win = {};
  var fooBar = {foo: 'foo', bar: 'bar'};
  goog.exportSymbol('one.two.four', fooBar, win);
  assertEquals(fooBar, win.one.two.four);
  assertTrue('four' in win.one.two);
  assertFalse('four' in one.two);
  one = undefined;
}

goog.exportSymbol('exceptionTest', function() {
  throw Error('ERROR');
});

function testExportSymbolExceptions() {
  var inner = function() {
    // If exceptionTest wasn't exported using execScript, IE8 will throw "Object
    // doesn't support this property or method" instead.
    exceptionTest();
  };
  var e = assertThrows('Exception wasn\'t thrown by exported function', inner);
  assertEquals('Unexpected error thrown', 'ERROR', e.message);
}

//=== tests for Require logic ===

function testRequireClosure() {
  assertNotUndefined('goog.Timer should be available', goog.Timer);
  /** @suppress {missingRequire} */
  assertNotUndefined(
      'goog.events.EventTarget should be available', goog.events.EventTarget);
}

function testRequireWithExternalDuplicate() {
  // alias to avoid the being picked up by the deps scanner.
  var provide = goog.provide;

  // Do a provide without going via goog.require. Then goog.require it
  // indirectly and ensure it doesn't cause a duplicate script.
  goog.addDependency('dup.js', ['dup.base'], []);
  goog.addDependency('dup-child.js', ['dup.base.child'], ['dup.base']);
  provide('dup.base');

  stubs.set(goog, 'isDocumentFinishedLoading_', false);
  stubs.set(goog.global, 'CLOSURE_IMPORT_SCRIPT', function(src) {
    if (src == goog.basePath + 'dup.js') {
      fail('Duplicate script written!');
    } else if (src == goog.basePath + 'dup-child.js') {
      // Allow expected script.
      return true;
    } else {
      // Avoid affecting other state.
      return false;
    }
  });

  // To differentiate this call from the real one.
  var require = goog.require;
  require('dup.base.child');
}

//=== tests for language enhancements ===

function testTypeOf() {
  assertEquals('array', goog.typeOf([]));
  assertEquals('string', goog.typeOf('string'));
  assertEquals('number', goog.typeOf(123));
  assertEquals('null', goog.typeOf(null));
  assertEquals('undefined', goog.typeOf(undefined));
  assertEquals('object', goog.typeOf({}));
  assertEquals('function', goog.typeOf(function() {}));

  // Make sure that NodeList is not treated as an array... NodeLists should
  // be of type object but Safari incorrectly reports it as function so a not
  // equals test will have to suffice here.
  assertNotEquals('array', goog.typeOf(document.getElementsByName('*')));
  assertNotEquals('function', goog.typeOf(document.getElementsByName('*')));
  assertEquals('object', goog.typeOf(document.getElementsByName('*')));
}

function testTypeOfFramed() {
  assertEquals('array', goog.typeOf(framedVars.array));
  assertEquals('string', goog.typeOf(framedVars.string));
  assertEquals('number', goog.typeOf(framedVars.number));
  assertEquals('null', goog.typeOf(framedVars.nullVar));
  assertEquals('undefined', goog.typeOf(framedVars.undefinedVar));
  assertEquals('object', goog.typeOf(framedVars.object));
  assertEquals('function', goog.typeOf(framedVars.functionVar));

  // Opera throws when trying to do cross frame typeof on node lists.
  // IE behaves very strange when it comes to DOM nodes on disconnected frames.
}

function testTypeOfFramed2() {
  assertEquals('array', goog.typeOf(framedVars2.array));
  assertEquals('string', goog.typeOf(framedVars2.string));
  assertEquals('number', goog.typeOf(framedVars2.number));
  assertEquals('null', goog.typeOf(framedVars2.nullVar));
  assertEquals('undefined', goog.typeOf(framedVars2.undefinedVar));
  assertEquals('object', goog.typeOf(framedVars2.object));
  assertEquals('function', goog.typeOf(framedVars2.functionVar));

  // Opera throws when trying to do cross frame typeof on node lists.
  // IE behaves very strange when it comes to DOM nodes on disconnected frames.
}

function testIsDef() {
  var defined = 'foo';
  var nullVar = null;
  var notDefined;

  assertTrue('defined should be defined', goog.isDef(defined));
  assertTrue('null should be defined', goog.isDef(nullVar));
  assertFalse('undefined should not be defined', goog.isDef(notDefined));
}

function testIsDefAndNotNull() {
  assertTrue('string is defined and non-null', goog.isDefAndNotNull(''));
  assertTrue('object is defined and non-null', goog.isDefAndNotNull({}));
  assertTrue(
      'function is defined and non-null',
      goog.isDefAndNotNull(goog.nullFunction));
  assertTrue('zero is defined and non-null', goog.isDefAndNotNull(0));
  assertFalse('null', goog.isDefAndNotNull(null));
  assertFalse('undefined', goog.isDefAndNotNull(undefined));
}

function testIsNull() {
  var notNull = 'foo';
  var nullVar = null;
  var notDefined;

  assertFalse('defined should not be null', goog.isNull(notNull));
  assertTrue('null should be null', goog.isNull(nullVar));
  assertFalse('undefined should not be null', goog.isNull(notDefined));
}

function testIsArray() {
  var array = [1, 2, 3];
  var arrayWithLengthSet = [1, 2, 3];
  arrayWithLengthSet.length = 2;
  var objWithArrayFunctions = {slice: function() {}, length: 0};
  var object = {a: 1, b: 2, c: 3};
  var nullVar = null;
  var notDefined;
  var elem = document.getElementById('elem');
  var text = document.getElementById('text').firstChild;
  var impostor = document.body.getElementsByTagName('BOGUS');
  impostor.push = Array.prototype.push;
  impostor.pop = Array.prototype.pop;
  impostor.slice = Array.prototype.slice;
  impostor.splice = Array.prototype.splice;

  assertTrue('array should be an array', goog.isArray(array));
  assertTrue(
      'arrayWithLengthSet should be an array',
      goog.isArray(arrayWithLengthSet));
  assertFalse(
      'object with array functions should not be an array unless ' +
          'length is not enumerable',
      goog.isArray(objWithArrayFunctions));
  assertFalse('object should not be an array', goog.isArray(object));
  assertFalse('null should not be an array', goog.isArray(nullVar));
  assertFalse('undefined should not be an array', goog.isArray(notDefined));
  assertFalse('NodeList should not be an array', goog.isArray(elem.childNodes));
  assertFalse('TextNode should not be an array', goog.isArray(text));
  assertTrue(
      'Array of nodes should be an array',
      goog.isArray([elem.firstChild, elem.lastChild]));
  assertFalse('An impostor should not be an array', goog.isArray(impostor));
}

function testTypeOfAcrossWindow() {
  if (goog.userAgent.IE && goog.userAgent.isVersionOrHigher('10') &&
      !goog.userAgent.isVersionOrHigher('11')) {
    // TODO(johnlenz): This test is flaky on IE10 (passing 90+% of the time).
    // When it flakes the values are undefined which appears to indicate the
    // script did not run in the opened window and not a failure of the logic
    // we are trying to test.
    return;
  }

  var w = window.open('', 'blank');
  if (w) {
    try {
      var d = w.document;
      d.open();
      d.write(
          '<script>function fun(){};' +
          'var arr = [];' +
          'var x = 42;' +
          'var s = "";' +
          'var b = true;' +
          'var obj = {length: 0, splice: {}, call: {}};' +
          '</' +
          'script>');
      d.close();

      assertEquals('function', goog.typeOf(w.fun));
      assertEquals('array', goog.typeOf(w.arr));
      assertEquals('number', goog.typeOf(w.x));
      assertEquals('string', goog.typeOf(w.s));
      assertEquals('boolean', goog.typeOf(w.b));
      assertEquals('object', goog.typeOf(w.obj));
    } finally {
      w.close();
    }
  }
}

function testIsArrayLike() {
  var array = [1, 2, 3];
  var objectWithNumericLength = {length: 2};
  var objectWithNonNumericLength = {length: 'a'};
  var object = {a: 1, b: 2};
  var nullVar = null;
  var notDefined;
  var elem = document.getElementById('elem');
  var text = document.getElementById('text').firstChild;

  assertTrue('array should be array-like', goog.isArrayLike(array));
  assertTrue(
      'obj w/numeric length should be array-like',
      goog.isArrayLike(objectWithNumericLength));
  assertFalse(
      'obj w/non-numeric length should not be array-like',
      goog.isArrayLike(objectWithNonNumericLength));
  assertFalse('object should not be array-like', goog.isArrayLike(object));
  assertFalse('null should not be array-like', goog.isArrayLike(nullVar));
  assertFalse(
      'undefined should not be array-like', goog.isArrayLike(notDefined));
  assertTrue(
      'NodeList should be array-like', goog.isArrayLike(elem.childNodes));
  // TODO(attila): Fix isArrayLike to return false for text nodes!
  // assertFalse('TextNode should not be array-like', goog.isArrayLike(text));
  assertTrue(
      'Array of nodes should be array-like',
      goog.isArrayLike([elem.firstChild, elem.lastChild]));
}


/**
 * Use mock date in testIsDateLike() rather than a real goog.date.Date to
 * minimize dependencies in this unit test.
 */
function MockGoogDate() {}

/** @return {number} */
MockGoogDate.prototype.getFullYear = function() {
  return 2007;
};


function testIsDateLike() {
  var jsDate = new Date();
  var googDate = new MockGoogDate();
  var string = 'foo';
  var number = 1;
  var nullVar = null;
  var notDefined;

  assertTrue('js Date should be date-like', goog.isDateLike(jsDate));
  assertTrue('goog Date should be date-like', goog.isDateLike(googDate));
  assertFalse('string should not be date-like', goog.isDateLike(string));
  assertFalse('number should not be date-like', goog.isDateLike(number));
  assertFalse('nullVar should not be date-like', goog.isDateLike(nullVar));
  assertFalse('undefined should not be date-like', goog.isDateLike(notDefined));
}

function testIsString() {
  var string = 'foo';
  var number = 2;
  var nullVar = null;
  var notDefined;

  assertTrue('string should be a string', goog.isString(string));
  assertFalse('number should not be a string', goog.isString(number));
  assertFalse('null should not be a string', goog.isString(nullVar));
  assertFalse('undefined should not be a string', goog.isString(notDefined));
}

function testIsBoolean() {
  var b = true;
  var s = 'true';
  var num = 1;
  var nullVar = null;
  var notDefined;

  assertTrue('boolean should be a boolean', goog.isBoolean(b));
  assertFalse('string should not be a boolean', goog.isBoolean(s));
  assertFalse('number should not be a boolean', goog.isBoolean(num));
  assertFalse('null should not be a boolean', goog.isBoolean(nullVar));
  assertFalse('undefined should not be a boolean', goog.isBoolean(notDefined));
}

function testIsNumber() {
  var number = 1;
  var string = '1';
  var nullVar = null;
  var notDefined;

  assertTrue('number should be a number', goog.isNumber(number));
  assertFalse('string should not be a number', goog.isNumber(string));
  assertFalse('null should not be a number', goog.isNumber(nullVar));
  assertFalse('undefined should not be a number', goog.isNumber(notDefined));
}

function testIsFunction() {
  var func = function() {
    return 1;
  };
  var object = {a: 1, b: 2};
  var nullVar = null;
  var notDefined;

  assertTrue('function should be a function', goog.isFunction(func));
  assertFalse('object should not be a function', goog.isFunction(object));
  assertFalse('null should not be a function', goog.isFunction(nullVar));
  assertFalse(
      'undefined should not be a function', goog.isFunction(notDefined));
}

function testIsObject() {
  var object = {a: 1, b: 2};
  var string = 'b';
  var nullVar = null;
  var notDefined;
  var array = [0, 1, 2];
  var fun = function() {};

  assertTrue('object should be an object', goog.isObject(object));
  assertTrue('array should be an object', goog.isObject(array));
  assertTrue('function should be an object', goog.isObject(fun));
  assertFalse('string should not be an object', goog.isObject(string));
  assertFalse('null should not be an object', goog.isObject(nullVar));
  assertFalse('undefined should not be an object', goog.isObject(notDefined));
}


//=== tests for unique ID methods ===

function testGetUid() {
  var a = {};
  var b = {};
  var c = {};

  var uid1 = goog.getUid(a);
  var uid2 = goog.getUid(b);
  var uid3 = goog.getUid(c);

  assertNotEquals('Unique IDs must be unique', uid1, uid2);
  assertNotEquals('Unique IDs must be unique', uid1, uid3);
  assertNotEquals('Unique IDs must be unique', uid2, uid3);
}

function testHasUid() {
  var a = {};

  assertFalse(goog.hasUid(a));
  assertFalse(goog.UID_PROPERTY_ in a);

  var uid = goog.getUid(a);
  assertTrue(goog.hasUid(a));
  assertEquals(uid, goog.getUid(a));
}

function testRemoveUidFromPlainObject() {
  var a = {};
  var uid = goog.getUid(a);
  goog.removeUid(a);
  assertNotEquals(
      'An object\'s old and new unique IDs should be different', uid,
      goog.getUid(a));
}

function testRemoveUidFromObjectWithoutUid() {
  var a = {};
  // Removing a unique ID should not fail even if it did not exist
  goog.removeUid(a);
}

function testRemoveUidFromNode() {
  var node = goog.dom.createElement(goog.dom.TagName.DIV);
  var nodeUid = goog.getUid(node);
  goog.removeUid(node);
  assertNotEquals(
      'A node\'s old and new unique IDs should be different', nodeUid,
      goog.getUid(node));
}

function testConstructorUid() {
  function BaseClass() {}
  function SubClass() {}
  goog.inherits(SubClass, BaseClass);

  var baseClassUid = goog.getUid(BaseClass);
  var subClassUid = goog.getUid(SubClass);

  assertTrue(
      'Unique ID of BaseClass must be a number',
      typeof baseClassUid == 'number');
  assertTrue(
      'Unique ID of SubClass must be a number', typeof subClassUid == 'number');
  assertNotEquals(
      'Unique IDs of BaseClass and SubClass must differ', baseClassUid,
      subClassUid);
  assertNotEquals(
      'Unique IDs of BaseClass and SubClass instances must differ',
      goog.getUid(new BaseClass), goog.getUid(new SubClass));

  assertEquals(
      'Unique IDs of BaseClass.prototype and SubClass.prototype ' +
          'should differ, but to keep the implementation simple, we do not ' +
          'handle this edge case.',
      goog.getUid(BaseClass.prototype), goog.getUid(SubClass.prototype));
}


/**
 * Tests against Chrome bug where the re-created element will have the uid
 * property set but undefined. See bug 1252508.
 */
function testUidNotUndefinedOnReusedElement() {
  var div = goog.dom.createElement(goog.dom.TagName.DIV);
  document.body.appendChild(div);
  div.innerHTML = '<form id="form"></form>';
  var span = goog.dom.getElementsByTagName(goog.dom.TagName.FORM, div)[0];
  goog.getUid(span);

  div.innerHTML = '<form id="form"></form>';
  var span2 = goog.dom.getElementsByTagName(goog.dom.TagName.FORM, div)[0];
  assertNotUndefined(goog.getUid(span2));
}

function testWindowUid() {
  var uid = goog.getUid(window);
  assertTrue('window unique id is a number', goog.isNumber(uid));
  assertEquals('returns the same id second time', uid, goog.getUid(window));
  goog.removeUid(window);
  assertNotEquals(
      'generates new id after the old one is removed', goog.getUid(window));
}

//=== tests for clone method ===

function testClonePrimitive() {
  assertEquals(
      'cloning a primitive should return an equal primitive', 5,
      goog.cloneObject(5));
}

function testCloneObjectThatHasACloneMethod() {
  var original = {
    name: 'original',
    clone: function() {
      return {name: 'clone'};
    }
  };

  var clone = goog.cloneObject(original);
  assertEquals('original', original.name);
  assertEquals('clone', clone.name);
}

function testCloneFlatObject() {
  var original = {a: 1, b: 2, c: 3};
  var clone = goog.cloneObject(original);
  assertNotEquals(original, clone);
  assertEquals(1, clone.a);
  assertEquals(2, clone.b);
  assertEquals(3, clone.c);
}

function testCloneDeepObject() {
  var original = {a: 1, b: {c: 2, d: 3}, e: {f: {g: 4, h: 5}}};
  var clone = goog.cloneObject(original);

  assertNotEquals(original, clone);
  assertNotEquals(original.b, clone.b);
  assertNotEquals(original.e, clone.e);

  assertEquals(1, clone.a);
  assertEquals(2, clone.b.c);
  assertEquals(3, clone.b.d);
  assertEquals(4, clone.e.f.g);
  assertEquals(5, clone.e.f.h);
}

function testCloneFunctions() {
  var original = {
    f: function() {
      return 'hi';
    }
  };
  var clone = goog.cloneObject(original);

  assertNotEquals(original, clone);
  assertEquals('hi', clone.f());
  assertEquals(original.f, clone.f);
}


//=== tests for bind() and friends ===

// Function.prototype.bind and Function.prototype.partial are purposefullly
// not defined in open sourced Closure.  These functions sniff for their
// presence.

var foo = 'global';
var obj = {foo: 'obj'};

/**
 * @param {?} arg1
 * @param {?} arg2
 * @return {?}
 */
function getFoo(arg1, arg2) {
  return {foo: this.foo, arg1: arg1, arg2: arg2};
}

function testBindWithoutObj() {
  if (Function.prototype.bind) {
    assertEquals(foo, getFoo.bind()().foo);
  }
}

function testBindWithObj() {
  if (Function.prototype.bind) {
    assertEquals(obj.foo, getFoo.bind(obj)().foo);
  }
}

function testBindWithNullObj() {
  if (Function.prototype.bind) {
    assertEquals(foo, getFoo.bind()().foo);
  }
}

function testBindStaticArgs() {
  if (Function.prototype.bind) {
    var fooprime = getFoo.bind(obj, 'hot', 'dog');
    var res = fooprime();
    assertEquals(obj.foo, res.foo);
    assertEquals('hot', res.arg1);
    assertEquals('dog', res.arg2);
  }
}

function testBindDynArgs() {
  if (Function.prototype.bind) {
    var res = getFoo.bind(obj)('hot', 'dog');
    assertEquals(obj.foo, res.foo);
    assertEquals('hot', res.arg1);
    assertEquals('dog', res.arg2);
  }
}

function testBindCurriedArgs() {
  if (Function.prototype.bind) {
    var res = getFoo.bind(obj, 'hot')('dog');
    assertEquals(obj.foo, res.foo);
    assertEquals('hot', res.arg1);
    assertEquals('dog', res.arg2);
  }
}

function testBindDoubleBind() {
  var getFooP = goog.bind(getFoo, obj, 'hot');
  var getFooP2 = goog.bind(getFooP, null, 'dog');

  var res = getFooP2();
  assertEquals('res.arg1 should be \'hot\'', 'hot', res.arg1);
  assertEquals('res.arg2 should be \'dog\'', 'dog', res.arg2);
}

function testBindWithCall() {
  var obj = {};
  var obj2 = {};
  var f = function() {
    assertEquals('this should be bound to obj', obj, this);
  };
  var b = goog.bind(f, obj);
  b.call(null);
  b.call(obj2);
}

function testBindJs() {
  assertEquals(1, goog.bindJs_(add, {
    valueOf: function() {
      return 1;
    }
  })());
  assertEquals(3, goog.bindJs_(add, null, 1, 2)());
}

function testBindNative() {
  if (Function.prototype.bind &&
      Function.prototype.bind.toString().indexOf('native code') != -1) {
    assertEquals(1, goog.bindNative_(add, {
      valueOf: function() {
        return 1;
      }
    })());
    assertEquals(3, goog.bindNative_(add, null, 1, 2)());

    assertThrows(function() {
      goog.bindNative_(null, null);
    });
  }
}

function testBindDefault() {
  assertEquals(1, goog.bind(add, {
    valueOf: function() {
      return 1;
    }
  })());
  assertEquals(3, goog.bind(add, null, 1, 2)());
}

/**
 * @param {...?} var_args
 * @return {?}
 */
function add(var_args) {
  var sum = Number(this) || 0;
  for (var i = 0; i < arguments.length; i++) {
    sum += arguments[i];
  }
  return sum;
}

function testPartial() {
  var f = function(x, y) {
    return x + y;
  };
  var g = goog.partial(f, 1);
  assertEquals(3, g(2));

  var h = goog.partial(f, 1, 2);
  assertEquals(3, h());

  var i = goog.partial(f);
  assertEquals(3, i(1, 2));
}

function testPartialUsesGlobal() {
  var f = function(x, y) {
    assertEquals(goog.global, this);
    return x + y;
  };
  var g = goog.partial(f, 1);
  var h = goog.partial(g, 2);
  assertEquals(3, h());
}

function testPartialWithCall() {
  var obj = {};
  var f = function(x, y) {
    assertEquals(obj, this);
    return x + y;
  };
  var g = goog.partial(f, 1);
  var h = goog.partial(g, 2);
  assertEquals(3, h.call(obj));
}

function testPartialAndBind() {
  // This ensures that this "survives" through a partial.
  var p = goog.partial(getFoo, 'hot');
  var b = goog.bind(p, obj, 'dog');

  var res = b();
  assertEquals(obj.foo, res.foo);
  assertEquals('hot', res.arg1);
  assertEquals('dog', res.arg2);
}

function testBindAndPartial() {
  // This ensures that this "survives" through a partial.
  var b = goog.bind(getFoo, obj, 'hot');
  var p = goog.partial(b, 'dog');

  var res = p();
  assertEquals(obj.foo, res.foo);
  assertEquals('hot', res.arg1);
  assertEquals('dog', res.arg2);
}

function testPartialMultipleCalls() {
  var f = goog.testing.recordFunction();

  var a = goog.partial(f, 'foo');
  var b = goog.partial(a, 'bar');

  a();
  a();
  b();
  b();

  assertEquals(4, f.getCallCount());

  var calls = f.getCalls();
  assertArrayEquals(['foo'], calls[0].getArguments());
  assertArrayEquals(['foo'], calls[1].getArguments());
  assertArrayEquals(['foo', 'bar'], calls[2].getArguments());
  assertArrayEquals(['foo', 'bar'], calls[3].getArguments());
}

function testGlobalEval() {
  goog.globalEval('var foofoofoo = 125;');
  assertEquals('Var should be globally assigned', 125, goog.global.foofoofoo);
  var foofoofoo = 128;
  assertEquals('Global should not have changed', 125, goog.global.foofoofoo);

  // NOTE(user): foofoofoo would normally be available in the function scope,
  // via the scope chain, but the JsUnit framework seems to do something weird
  // which makes it not work.
}

function testGlobalEvalWithHtml() {
  // Make sure we don't trip on HTML markup in the code
  goog.global.evalTestResult = 'failed';
  goog.global.evalTest = function(arg) {
    goog.global.evalTestResult = arg;
  };

  goog.globalEval('evalTest("<test>")');

  assertEquals(
      'Should be able to evaluate strings with HTML in', '<test>',
      goog.global.evalTestResult);
}


//=== tests for inherits ===

function testInherits() {
  function Foo() {}
  function Bar() {}
  goog.inherits(Bar, Foo);
  var bar = new Bar();

  assert('object should be instance of constructor', bar instanceof Bar);
  assert('object should be instance of base constructor', bar instanceof Foo);
}

function testInherits_constructor() {
  function Foo() {}
  function Bar() {}
  goog.inherits(Bar, Foo);
  var bar = new Bar();

  assertEquals(
      'constructor property should match constructor function', Bar,
      bar.constructor);
  assertEquals(
      'Superclass constructor should match constructor function', Foo,
      Bar.superClass_.constructor);
}


//=== tests for makeSingleton ===
function testMakeSingleton() {
  function Foo() {}
  goog.addSingletonGetter(Foo);

  assertNotNull('Should add get instance function', Foo.getInstance);

  var x = Foo.getInstance();
  assertNotNull('Should successfully create an object', x);

  var y = Foo.getInstance();
  assertEquals('Should return the same object', x, y);

  delete Foo.instance_;

  var z = Foo.getInstance();
  assertNotNull('Should work after clearing for testing', z);

  assertNotEquals(
      'Should return a different object after clearing for testing', x, z);
}


//=== tests for now ===

function testNow() {
  var toleranceMilliseconds = 20;  // 10 ms was not enough for IE7.
  var now1 = new Date().getTime();
  var now2 = goog.now();
  assertTrue(Math.abs(now1 - now2) < toleranceMilliseconds);
}


//=== test non-html context ===

function testInHtmlDocument() {
  var savedGoogGlobal = goog.global;
  try {
    goog.global = {};
    assertFalse(goog.inHtmlDocument_());
    goog.global.document = {};
    assertFalse(goog.inHtmlDocument_());
    goog.global.document.write = function() {};
    assertTrue(goog.inHtmlDocument_());
  } finally {
    // Restore context to respect other tests.
    goog.global = savedGoogGlobal;
  }
}

function testLoadInNonHtmlNotThrows() {
  var savedGoogGlobal = goog.global;
  try {
    goog.global = {};
    goog.global.document = {};
    assertFalse(goog.inHtmlDocument_());
    // The goog code which is executed at load.
    goog.findBasePath_();
    goog.writeScriptTag_(goog.basePath + 'deps.js');
  } finally {
    // Restore context to respect other tests.
    goog.global = savedGoogGlobal;
  }
}

function testLoadBaseWithQueryParamOk() {
  var savedGoogGlobal = goog.global;
  try {
    goog.global = {};
    goog.global.document = {
      write: goog.nullFunction,
      getElementsByTagName:
          goog.functions.constant([{src: '/path/to/base.js?zx=5'}])
    };
    assertTrue(goog.inHtmlDocument_());
    goog.findBasePath_();
    assertEquals('/path/to/', goog.basePath);
  } finally {
    // Restore context to respect other tests.
    goog.global = savedGoogGlobal;
  }
}

function testLoadBaseFromGlobalVariableOk() {
  var savedGoogGlobal = goog.global;
  try {
    goog.global = {};
    goog.global.document = {
      write: goog.nullFunction,
      getElementsByTagName:
          goog.functions.constant([{src: '/path/to/base.js?zx=5'}])
    };
    goog.global.CLOSURE_BASE_PATH = '/from/constant/';
    goog.findBasePath_();
    assertEquals(goog.global.CLOSURE_BASE_PATH, goog.basePath);
  } finally {
    // Restore context to respect other tests.
    goog.global = savedGoogGlobal;
  }
}

function testLoadBaseFromGlobalVariableDOMClobbered() {
  var savedGoogGlobal = goog.global;
  try {
    goog.global = {};
    goog.global.document = {
      write: goog.nullFunction,
      getElementsByTagName:
          goog.functions.constant([{src: '/path/to/base.js?zx=5'}])
    };
    // Make goog.global.CLOSURE_BASE_PATH an object with a toString, like
    // it would be if it were a DOM clobbered HTMLElement.
    goog.global.CLOSURE_BASE_PATH = {};
    goog.global.CLOSURE_BASE_PATH.toString = function() {
      return '/from/constant/';
    };
    goog.findBasePath_();
    assertEquals('/path/to/', goog.basePath);
  } finally {
    // Restore context to respect other tests.
    goog.global = savedGoogGlobal;
  }
}

function testLoadBaseFromCurrentScriptIgnoringOthers() {
  var savedGoogGlobal = goog.global;
  try {
    goog.global = {};
    goog.global.document = {
      write: goog.nullFunction,
      currentScript: {src: '/currentScript/base.js?zx=5'},
      getElementsByTagName:
          goog.functions.constant([{src: '/path/to/base.js?zx=5'}])
    };
    goog.findBasePath_();
    assertEquals('/currentScript/', goog.basePath);
  } finally {
    // Restore context to respect other tests.
    goog.global = savedGoogGlobal;
  }
}

//=== tests for getmsg ===
function testGetMsgWithDollarSigns() {
  var msg = goog.getMsg('{$amount} per minute', {amount: '$0.15'});
  assertEquals('$0.15 per minute', msg);
  msg = goog.getMsg('{$amount} per minute', {amount: '$0.$1$5'});
  assertEquals('$0.$1$5 per minute', msg);

  msg = goog.getMsg('This is a {$rate} sale!', {rate: '$$$$$$$$$$10'});
  assertEquals('This is a $$$$$$$$$$10 sale!', msg);
  msg = goog.getMsg(
      '{$name}! Hamburgers: {$hCost}, Hotdogs: {$dCost}.',
      {name: 'Burger Bob', hCost: '$0.50', dCost: '$100'});
  assertEquals('Burger Bob! Hamburgers: $0.50, Hotdogs: $100.', msg);
}


function testGetMsgWithPlaceholders() {
  var msg = goog.getMsg('{$a} has {$b}', {a: '{$b}', b: 1});
  assertEquals('{$b} has 1', msg);

  msg = goog.getMsg('{$a}{$b}', {b: ''});
  assertEquals('{$a}', msg);
}


//=== miscellaneous tests ===

function testGetObjectByName() {
  var m = {
    'undefined': undefined,
    'null': null,
    emptyString: '',
    'false': false,
    'true': true,
    zero: 0,
    one: 1,
    two: {three: 3, four: {five: 5}},
    'six|seven': '6|7',
    'eight.nine': 8.9
  };
  goog.global.m = m;

  assertNull(goog.getObjectByName('m.undefined'));
  assertNull(goog.getObjectByName('m.null'));
  assertEquals(goog.getObjectByName('m.emptyString'), '');
  assertEquals(goog.getObjectByName('m.false'), false);
  assertEquals(goog.getObjectByName('m.true'), true);
  assertEquals(goog.getObjectByName('m.zero'), 0);
  assertEquals(goog.getObjectByName('m.one'), 1);
  assertEquals(goog.getObjectByName('m.two.three'), 3);
  assertEquals(goog.getObjectByName('m.two.four.five'), 5);
  assertEquals(goog.getObjectByName('m.six|seven'), '6|7');
  assertNull(goog.getObjectByName('m.eight.nine'));
  assertNull(goog.getObjectByName('m.notThere'));

  assertEquals(goog.getObjectByName('one', m), 1);
  assertEquals(goog.getObjectByName('two.three', m), 3);
  assertEquals(goog.getObjectByName('two.four.five', m), 5);
  assertEquals(goog.getObjectByName('six|seven', m), '6|7');
  assertNull(goog.getObjectByName('eight.nine', m));
  assertNull(goog.getObjectByName('notThere', m));
}


function testGetCssName() {
  assertEquals('classname', goog.getCssName('classname'));
  assertEquals('random-classname', goog.getCssName('random-classname'));
  assertEquals('control-modifier', goog.getCssName('control', 'modifier'));

  goog.setCssNameMapping({'goog': 'a', 'disabled': 'b'}, 'BY_PART');
  var g = goog.getCssName('goog');
  assertEquals('a', g);
  assertEquals('a-b', goog.getCssName(g, 'disabled'));
  assertEquals('a-b', goog.getCssName('goog-disabled'));
  assertEquals('a-button', goog.getCssName('goog-button'));

  goog.setCssNameMapping({'goog-button': 'a', 'active': 'b'}, 'BY_WHOLE');

  g = goog.getCssName('goog-button');
  assertEquals('a', g);
  assertEquals('a-b', goog.getCssName(g, 'active'));
  assertEquals('goog-disabled', goog.getCssName('goog-disabled'));

  e = assertThrows(function() {
    goog.getCssName('.name');
  });
  assertEquals(
      'className passed in goog.getCssName must not start with ".".' +
          ' You passed: .name',
      e.message);

  assertNull(goog.getCssName(null));
}

function testGetCssName_nameMapFn() {
  assertEquals('classname', goog.getCssName('classname'));

  goog.global.CLOSURE_CSS_NAME_MAP_FN = function(classname) {
    return classname + '!';
  };

  assertEquals('classname!', goog.getCssName('classname'));
}

function testAddDependency() {
  stubs.set(goog, 'writeScriptTag_', goog.nullFunction);

  goog.addDependency('foo.js', ['testDep.foo'], ['testDep.bar']);

  // alias to avoid the being picked up by the deps scanner.
  var provide = goog.provide;

  provide('testDep.bar');

  // To differentiate this call from the real one.
  var require = goog.require;

  // this used to throw an exception
  require('testDep.foo');

  assertTrue(goog.isObject(testDep.bar));

  // Unset provided namespace so the test can be re-run.
  testDep = undefined;
}

function testAddDependencyModule() {
  var load = goog.testing.recordFunction();
  stubs.set(goog, 'writeScriptTag_', load);

  goog.addDependency('mod.js', ['testDep.mod'], [], true);
  goog.addDependency('empty.js', ['testDep.empty'], [], {});
  goog.addDependency('mod-goog.js', ['testDep.goog'], [], {'module': 'goog'});

  // To differentiate this call from the real one.
  var require = goog.require;

  var assertModuleLoad = function(module, args) {
    assertEquals(2, args.length);
    assertEquals('', args[0]);
    assertRegExp(
        '^goog\\.retrieveAndExec_\\(".*/' + module + '", true, false\\);$',
        args[1]);
  };

  require('testDep.mod');
  assertEquals(1, load.getCallCount());
  assertModuleLoad('mod.js', load.getCalls()[0].getArguments());

  require('testDep.empty');
  assertEquals(2, load.getCallCount());
  assertEquals(2, load.getCalls()[1].getArguments().length);
  assertRegExp('^.*/empty.js$', load.getCalls()[1].getArguments()[0]);
  assertUndefined(load.getCalls()[1].getArguments()[1]);

  require('testDep.goog');
  assertEquals(3, load.getCallCount());
  assertModuleLoad('mod-goog.js', load.getCalls()[2].getArguments());

  // Unset provided namespace so the test can be re-run.
  testDep = undefined;
}

function testAddDependencyEs6() {
  var script = null;
  var requireTranspilation = false;
  stubs.set(goog, 'needsTranspile_', function() {
    return requireTranspilation;
  });
  stubs.set(goog, 'writeScriptTag_', function(src, scriptText) {
    if (script != null) {
      throw new Error('Multiple scripts written');
    }
    script = scriptText;
  });

  goog.addDependency(
      'fancy.js', ['testDep.fancy'], [],
      {'lang': 'es6-impl', 'module': 'goog'});
  goog.addDependency('super.js', ['testDep.superFancy'], [], {'lang': 'es6'});

  // To differentiate this call from the real one.
  var require = goog.require;

  requireTranspilation = false;
  require('testDep.fancy');
  assertRegExp(
      /^goog\.retrieveAndExec_\(".*\/fancy\.js", true, false\);$/, script);
  script = null;

  requireTranspilation = true;
  require('testDep.superFancy');
  assertRegExp(
      /^goog\.retrieveAndExec_\(".*\/super\.js", false, true\);$/, script);

  // Unset provided namespace so the test can be re-run.
  testDep = undefined;
}

function testBaseMethod() {
  function A() {}
  A.prototype.foo = function(x, y) {
    return x + y;
  };

  function B() {}
  goog.inherits(B, A);
  B.prototype.foo = function(x, y) {
    return 2 + goog.base(this, 'foo', x, y);
  };

  function C() {}
  goog.inherits(C, B);
  C.prototype.foo = function(x, y) {
    return 4 + goog.base(this, 'foo', x, y);
  };

  var d = new C();
  d.foo = function(x, y) {
    return 8 + goog.base(this, 'foo', x, y);
  };

  assertEquals(15, d.foo(1, 0));
  assertEquals(16, d.foo(1, 1));
  assertEquals(16, d.foo(2, 0));
  assertEquals(7, (new C()).foo(1, 0));
  assertEquals(3, (new B()).foo(1, 0));
  assertThrows(function() {
    goog.base(d, 'foo', 1, 0);
  });

  delete B.prototype.foo;
  assertEquals(13, d.foo(1, 0));

  delete C.prototype.foo;
  assertEquals(9, d.foo(1, 0));
}

function testBaseMethodAndBaseCtor() {
  // This will fail on FF4.0 if the following bug is not fixed:
  // https://bugzilla.mozilla.org/show_bug.cgi?id=586482
  function A(x, y) {
    this.foo(x, y);
  }
  A.prototype.foo = function(x, y) {
    this.bar = x + y;
  };

  function B(x, y) {
    goog.base(this, x, y);
  }
  goog.inherits(B, A);
  B.prototype.foo = function(x, y) {
    goog.base(this, 'foo', x, y);
    this.bar = this.bar * 2;
  };

  assertEquals(14, new B(3, 4).bar);
}

function testBaseClass() {
  function A(x, y) {
    this.foo = x + y;
  }

  function B(x, y) {
    goog.base(this, x, y);
    this.foo += 2;
  }
  goog.inherits(B, A);

  function C(x, y) {
    goog.base(this, x, y);
    this.foo += 4;
  }
  goog.inherits(C, B);

  function D(x, y) {
    goog.base(this, x, y);
    this.foo += 8;
  }
  goog.inherits(D, C);

  assertEquals(15, (new D(1, 0)).foo);
  assertEquals(16, (new D(1, 1)).foo);
  assertEquals(16, (new D(2, 0)).foo);
  assertEquals(7, (new C(1, 0)).foo);
  assertEquals(3, (new B(1, 0)).foo);
}

function testClassBaseOnMethod() {
  function A() {}
  A.prototype.foo = function(x, y) {
    return x + y;
  };

  function B() {}
  goog.inherits(B, A);
  B.prototype.foo = function(x, y) {
    return 2 + B.base(this, 'foo', x, y);
  };

  function C() {}
  goog.inherits(C, B);
  C.prototype.foo = function(x, y) {
    return 4 + C.base(this, 'foo', x, y);
  };

  var d = new C();
  assertEquals(7, d.foo(1, 0));
  assertEquals(8, d.foo(1, 1));
  assertEquals(8, d.foo(2, 0));
  assertEquals(3, (new B()).foo(1, 0));

  delete B.prototype.foo;
  assertEquals(5, d.foo(1, 0));

  delete C.prototype.foo;
  assertEquals(1, d.foo(1, 0));
}

function testClassBaseOnConstructor() {
  function A(x, y) {
    this.foo = x + y;
  }

  function B(x, y) {
    B.base(this, 'constructor', x, y);
    this.foo += 2;
  }
  goog.inherits(B, A);

  function C(x, y) {
    C.base(this, 'constructor', x, y);
    this.foo += 4;
  }
  goog.inherits(C, B);

  function D(x, y) {
    D.base(this, 'constructor', x, y);
    this.foo += 8;
  }
  goog.inherits(D, C);

  assertEquals(15, (new D(1, 0)).foo);
  assertEquals(16, (new D(1, 1)).foo);
  assertEquals(16, (new D(2, 0)).foo);
  assertEquals(7, (new C(1, 0)).foo);
  assertEquals(3, (new B(1, 0)).foo);
}

function testClassBaseOnMethodAndBaseCtor() {
  function A(x, y) {
    this.foo(x, y);
  }
  A.prototype.foo = function(x, y) {
    this.bar = x + y;
  };

  function B(x, y) {
    B.base(this, 'constructor', x, y);
  }
  goog.inherits(B, A);
  B.prototype.foo = function(x, y) {
    B.base(this, 'foo', x, y);
    this.bar = this.bar * 2;
  };

  assertEquals(14, new B(3, 4).bar);
}

function testGoogRequireCheck() {
  // alias to avoid the being picked up by the deps scanner.
  var provide = goog.provide;

  stubs.set(goog, 'ENABLE_DEBUG_LOADER', true);
  stubs.set(goog, 'useStrictRequires', true);
  stubs.set(goog, 'implicitNamespaces_', {});

  // Aliased so that build tools do not mistake this for an actual call.
  var require = goog.require;
  assertThrows('Requiring non-required namespace should fail', function() {
    require('far.outnotprovided');
  });

  assertUndefined(goog.global.far);
  assertEvaluatesToFalse(goog.getObjectByName('far.out'));
  assertObjectEquals({}, goog.implicitNamespaces_);
  assertFalse(goog.isProvided_('far.out'));

  provide('far.out');

  assertNotUndefined(far.out);
  assertEvaluatesToTrue(goog.getObjectByName('far.out'));
  assertObjectEquals({'far': true}, goog.implicitNamespaces_);
  assertTrue(goog.isProvided_('far.out'));

  goog.global.far.out = 42;
  assertEquals(42, goog.getObjectByName('far.out'));
  assertTrue(goog.isProvided_('far.out'));

  // Empty string should be allowed.
  goog.global.far.out = '';
  assertEquals('', goog.getObjectByName('far.out'));
  assertTrue(goog.isProvided_('far.out'));

  // Null or undefined are not allowed.
  goog.global.far.out = null;
  assertNull(goog.getObjectByName('far.out'));
  assertFalse(goog.isProvided_('far.out'));

  goog.global.far.out = undefined;
  assertNull(goog.getObjectByName('far.out'));
  assertFalse(goog.isProvided_('far.out'));

  stubs.reset();
  delete far;
}

/**
 * @return {?}
 */
function diables_testCspSafeGoogRequire() {
  if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('10')) {
    return;
  }

  stubs.set(goog, 'ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING', true);

  // Aliased so that build tools do not mistake this for an actual call.
  var require = goog.require;

  require('goog.Uri');

  // Set a timeout to allow the user agent to finish parsing this script block,
  // thus allowing the appended script (via goog.require) to execute.
  var ASYNC_TIMEOUT_MS = 1000;

  var resolver = goog.Promise.withResolver();
  window.setTimeout(function() {
    assertNotUndefined(goog.Uri);
    resolver.resolve();
    stubs.reset();
  }, ASYNC_TIMEOUT_MS);

  return resolver.promise;
}

function testLateRequireProtection() {
  if (!document.readyState) return;
  var e = assertThrows(function() {
    // To differentiate this call from the real one.
    var require = goog.require;
    require('goog.ui.Component');
  });

  assertContains('after document load', e.message);
}

function testDefineClass() {
  var Base = goog.defineClass(null, {
    constructor: function(foo) {
      this.foo = foo;
    },
    statics: {x: 42},
    frobnicate: function() {
      return this.foo + this.foo;
    }
  });
  var Derived = goog.defineClass(Base, {
    constructor: function() {
      Derived.base(this, 'constructor', 'bar');
    },
    frozzle: function(foo) {
      this.foo = foo;
    }
  });

  assertEquals(42, Base.x);
  var der = new Derived();
  assertEquals('barbar', der.frobnicate());
  der.frozzle('qux');
  assertEquals('quxqux', der.frobnicate());
}

function testDefineClass_interface() {
  /** @interface */
  var Interface =
      goog.defineClass(null, {statics: {foo: 'bar'}, qux: function() {}});
  assertEquals('bar', Interface.foo);
  assertThrows(function() {
    new Interface();
  });
}

function testDefineClass_seals() {
  if (!(Object.seal instanceof Function)) return;  // IE<9 doesn't have seal
  var A = goog.defineClass(null, {constructor: function() {}});
  var a = new A();
  try {
    a.foo = 'bar';
  } catch (expectedInStrictModeOnly) { /* ignored */
  }
  assertEquals(undefined, a.foo);
}

function testDefineClass_unsealable() {
  var LegacyBase = function() {};
  LegacyBase.prototype.foo = null;
  LegacyBase.prototype.setFoo = function(foo) {
    this.foo = foo;
  };
  goog.tagUnsealableClass(LegacyBase);

  var Derived = goog.defineClass(LegacyBase, {constructor: function() {}});

  var der = new Derived();
  der.setFoo('bar');
  assertEquals('bar', der.foo);
}

function testDefineClass_constructorIsNotWrappedWhenSealingIsDisabled() {
  var org = goog.defineClass;
  var ctr = null;
  var replacement = function(superClass, def) {
    ctr = def.constructor;
    return org(superClass, def);
  };
  // copy all the properties
  goog.object.extend(replacement, org);
  replacement.SEAL_CLASS_INSTANCES = false;

  stubs.replace(goog, 'defineClass', replacement);
  var MyClass = goog.defineClass(null, {constructor: function() {}});
  assertEquals('The constructor should not be wrapped.', ctr, MyClass);
}

function testDefineClass_unsealableConstructorIsWrapped() {
  var LegacyBase = function() {};
  LegacyBase.prototype.foo = null;
  LegacyBase.prototype.setFoo = function(foo) {
    this.foo = foo;
  };
  goog.tagUnsealableClass(LegacyBase);

  var org = goog.defineClass;
  var ctr = null;
  var replacement = function(superClass, def) {
    ctr = def.constructor;
    return org(superClass, def);
  };
  // copy all the properties
  goog.object.extend(replacement, org);

  stubs.replace(goog, 'defineClass', replacement);
  var Derived = goog.defineClass(LegacyBase, {constructor: function() {}});

  assertNotEquals('The constructor should be wrapped.', ctr, Derived);
}

// Validate the behavior of goog.module when used from traditional files.
function testGoogModuleGet() {
  // assert that goog.module doesn't modify the global namespace
  assertUndefined(
      'module failed to protect global namespace: ' +
          'goog.test_module_dep',
      goog.test_module_dep);

  // assert that goog.module with goog.module.declareLegacyNamespace is present.
  assertNotUndefined(
      'module failed to declare global namespace: ' +
          'goog.test_module',
      goog.test_module);

  // assert that a require'd goog.module is available immediately after the
  // goog.require call.
  assertNotUndefined(
      'module failed to protect global namespace: ' +
          'goog.test_module_dep',
      earlyTestModuleGet);


  // assert that an non-existent module request doesn't throw and returns null.
  assertEquals(null, goog.module.get('unrequired.module.id'));

  // Validate the module exports
  var testModuleExports = goog.module.get('goog.test_module');
  assertTrue(goog.isFunction(testModuleExports));

  // Test that any escaping of </script> in test files is correct. Escape the
  // / in </script> here so that any such code does not affect it here.
  assertEquals('<\/script>', testModuleExports.CLOSING_SCRIPT_TAG);

  // Validate that the module exports object has not changed
  assertEquals(earlyTestModuleGet, testModuleExports);
}


// Validate the behavior of goog.module when used from traditional files.
function testGoogLoadModuleByUrl() {
  if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('10')) {
    // IE before 10 don't report an error.
    return;
  }

  stubs.set(goog, 'loadFileSync_', function(src) {
    return 'closure load file sync: ' + src;
  });

  // "goog.loadModuleByUrl" is not a general purpose code loader, it can
  // not be used to late load code.
  var err =
      assertThrows('loadModuleFromUrl should not hide failures', function() {
        goog.loadModuleFromUrl('bogus url');
      });
  assertContains('Cannot write "bogus url" after document load', err.message);
}


function testModuleExportSealed() {
  if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9')) {
    // IE before 9 don't support sealing objects
    return;
  }

  goog.loadModule('goog.module("a.b.supplied"); exports.foo = {};');
  var exports0 = goog.module.get('a.b.supplied');
  assertTrue(Object.isSealed(exports0));

  goog.loadModule('goog.module("a.b.object"); exports = {};');
  var exports1 = goog.module.get('a.b.object');
  assertTrue(Object.isSealed(exports1));


  goog.loadModule('goog.module("a.b.fn"); exports = function() {};');
  var exports2 = goog.module.get('a.b.fn');
  assertFalse(Object.isSealed(exports2));
}

function testWorkaroundSafari10EvalBug0() {
  // Validate the safari module loading workaround isn't triggered for
  // browsers we know it isn't needed.
  if (goog.userAgent.SAFARI) {
    return;
  }
  assertFalse(goog.useSafari10Workaround());
}

function testWorkaroundSafari10EvalBug1() {
  assertEquals(
      '(function(){' +  // no \n
          'goog.module(\'foo\');\n' +
          '\n;})();\n',
      goog.workaroundSafari10EvalBug(
          'goog.module(\'foo\');\n'));
}


function testWorkaroundSafari10EvalBug2() {
  assertEquals(
      '(function(){' +  // no \n
          'goog.module(\'foo\');\n' +
          'alert("//# sourceMappingURL a.b.c.map")\n' +
          'alert("//# sourceURL a.b.c.js")\n' +
          '\n;})();\n',
      goog.workaroundSafari10EvalBug(
          'goog.module(\'foo\');\n' +
          'alert("//# sourceMappingURL a.b.c.map")\n' +
          'alert("//# sourceURL a.b.c.js")\n'));
}

function testGoogLoadModuleInSafari10() {
  try {
    eval('let es6 = 1');
  } catch (e) {
    // If ES6 block scope syntax isn't supported, don't run the rest of the
    // test.
    return;
  }

  goog.loadModule(
      'goog.module("a.safari.test");' +
      'let x = true;' +
      'function fn() { return x }' +
      'exports.fn = fn;');
  var exports = goog.module.get('a.safari.test');

  // Safari 10 will throw an exception if the module being loaded is eval'd
  // without a containing function.
  assertNotThrows(exports.fn);
}


function testLoadFileSync() {
  var fileContents = goog.loadFileSync_('deps.js');
  assertTrue(
      'goog.loadFileSync_ returns string', typeof fileContents === 'string');
  assertTrue('goog.loadFileSync_ string length > 0', fileContents.length > 0);

  stubs.set(goog.global, 'CLOSURE_LOAD_FILE_SYNC', function(src) {
    return 'closure load file sync: ' + src;
  });

  assertEquals(
      'goog.CLOSURE_LOAD_FILE_SYNC override', goog.loadFileSync_('test url'),
      'closure load file sync: test url');
}


function testNormalizePath1() {
  assertEquals('foo/path.js', goog.normalizePath_('./foo/./path.js'));
  assertEquals('foo/path.js', goog.normalizePath_('bar/../foo/path.js'));
  assertEquals('bar/path.js', goog.normalizePath_('bar/foo/../path.js'));
  assertEquals('path.js', goog.normalizePath_('bar/foo/../../path.js'));

  assertEquals('../foo/path.js', goog.normalizePath_('../foo/path.js'));
  assertEquals('../../foo/path.js', goog.normalizePath_('../../foo/path.js'));
  assertEquals('../path.js', goog.normalizePath_('../foo/../path.js'));
  assertEquals('../../path.js', goog.normalizePath_('../foo/../../path.js'));

  assertEquals('/../foo/path.js', goog.normalizePath_('/../foo/path.js'));
  assertEquals('/path.js', goog.normalizePath_('/foo/../path.js'));
  assertEquals('/foo/path.js', goog.normalizePath_('/./foo/path.js'));

  assertEquals('//../foo/path.js', goog.normalizePath_('//../foo/path.js'));
  assertEquals('//path.js', goog.normalizePath_('//foo/../path.js'));
  assertEquals('//foo/path.js', goog.normalizePath_('//./foo/path.js'));

  assertEquals('http://../x/y.js', goog.normalizePath_('http://../x/y.js'));
  assertEquals('http://path.js', goog.normalizePath_('http://foo/../path.js'));
  assertEquals('http://x/path.js', goog.normalizePath_('http://./x/path.js'));
}




function testGoogModuleNames() {
  // avoid usage checks
  var module = goog.module;

  function assertInvalidId(id) {
    var err = assertThrows(function() {
      module(id);
    });
    assertEquals('Invalid module identifier', err.message);
  }

  function assertValidId(id) {
    // This is a cheesy check, but we validate that we don't get an invalid
    // namespace warning, but instead get a module isn't loaded correctly
    // error.
    var err = assertThrows(function() {
      module(id);
    });
    assertTrue(err.message.indexOf('has been loaded incorrectly') != -1);
  }

  assertInvalidId('/somepath/module.js');
  assertInvalidId('./module.js');
  assertInvalidId('1');

  assertValidId('a');
  assertValidId('a.b');
  assertValidId('a.b.c');
  assertValidId('aB.Cd.eF');
  assertValidId('a1.0E.Fg');

  assertValidId('_');
  assertValidId('$');
  assertValidId('_$');
  assertValidId('$_');
}