/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/** @fileoverview Shared unit tests for styles. */

goog.module('goog.style_test');
goog.setTestOnly();

const Box = goog.require('goog.math.Box');
const BrowserEvent = goog.require('goog.events.BrowserEvent');
const Const = goog.require('goog.string.Const');
const Coordinate = goog.require('goog.math.Coordinate');
const ExpectedFailures = goog.require('goog.testing.ExpectedFailures');
const GoogRect = goog.require('goog.math.Rect');
const MockUserAgent = goog.require('goog.testing.MockUserAgent');
const Size = goog.require('goog.math.Size');
const TagName = goog.require('goog.dom.TagName');
const TrustedResourceUrl = goog.require('goog.html.TrustedResourceUrl');
const UserAgents = goog.require('goog.userAgentTestUtil.UserAgents');
const color = goog.require('goog.color');
const dispose = goog.require('goog.dispose');
const googArray = goog.require('goog.array');
const googDom = goog.require('goog.dom');
const googObject = goog.require('goog.object');
const googStyle = goog.require('goog.style');
const isSafari = goog.require('goog.labs.userAgent.browser');
const jsunit = goog.require('goog.testing.jsunit');
const safe = goog.require('goog.dom.safe');
const testSuite = goog.require('goog.testing.testSuite');
const testing = goog.require('goog.html.testing');
const userAgent = goog.require('goog.userAgent');
const userAgentTestUtil = goog.require('goog.userAgentTestUtil');

// Delay running the tests after page load. This test has some asynchronous
// behavior that interacts with page load detection.
/** @suppress {constantProperty} suppression added to enable type checking */
jsunit.AUTO_RUN_DELAY_IN_MS = 500;

const isBorderBox = !googDom.isCss1CompatMode();
const EPSILON = 2;
let expectedFailures;
const $ = googDom.getElement;
let mockUserAgent;

function hasWebkitTransform() {
  return 'webkitTransform' in document.body.style;
}

function assertColorRgbEquals(expected, actual) {
  assertEquals(expected, color.hexToRgbStyle(color.parse(actual).hex));
}

/**
 * Asserts that the coordinate is approximately equal to the given
 * x and y coordinates, give or take delta.
 */
function assertCoordinateApprox(x, y, delta, coord) {
  assertTrue(
      `Expected x: ${x}, actual x: ` + coord.x,
      coord.x >= x - delta && coord.x <= x + delta);
  assertTrue(
      `Expected y: ${y}, actual y: ` + coord.y,
      coord.y >= y - delta && coord.y <= y + delta);
}

/**
 * Test browser detection for a user agent configuration.
 * @param {Array<number>} expectedAgents Array of expected userAgents.
 * @param {string} uaString User agent string.
 * @param {string=} product Navigator product string.
 * @param {string=} vendor Navigator vendor string.
 */
function assertUserAgent(
    expectedAgents, uaString, product = undefined, vendor = undefined) {
  const mockNavigator = {
    'userAgent': uaString,
    'product': product,
    'vendor': vendor,
  };

  mockUserAgent.setNavigator(mockNavigator);
  mockUserAgent.setUserAgentString(uaString);

  userAgentTestUtil.reinitializeUserAgent();
  for (const ua in UserAgents) {
    const isExpected = googArray.contains(expectedAgents, UserAgents[ua]);
    assertEquals(
        isExpected, userAgentTestUtil.getUserAgentDetected(UserAgents[ua]));
  }
}

testSuite({
  setUpPage() {
    expectedFailures = new ExpectedFailures();

    const viewportSize = googDom.getViewportSize();
    // When the window is too short or not wide enough, some tests, especially
    // those for off-screen elements, fail.  Oddly, the most reliable
    // indicator is a width of zero (which is of course erroneous), since
    // height sometimes includes a scroll bar.  We can make no assumptions on
    // window size on the Selenium farm.
    if (userAgent.IE && viewportSize.width < 300) {
      // Move to origin, since IE won't resize outside the screen.
      window.moveTo(0, 0);
      window.resizeTo(640, 480);
    }
  },

  setUp() {
    window.scrollTo(0, 0);
    userAgentTestUtil.reinitializeUserAgent();
    mockUserAgent = new MockUserAgent();
    mockUserAgent.install();
  },

  tearDown() {
    expectedFailures.handleTearDown();
    const testVisibleDiv2 = googDom.getElement('test-visible2');
    testVisibleDiv2.setAttribute('style', '');
    googDom.removeChildren(testVisibleDiv2);
    const testViewport = googDom.getElement('test-viewport');
    testViewport.setAttribute('style', '');
    googDom.removeChildren(testViewport);
    dispose(mockUserAgent);

    // Prevent multiple vendor prefixed mock elements from poisoning the cache.
    /** @suppress {visibility} suppression added to enable type checking */
    googStyle.styleNameCache_ = {};
  },

  testSetStyle() {
    const el = $('span1');
    googStyle.setStyle(el, 'textDecoration', 'underline');
    assertEquals('Should be underline', 'underline', el.style.textDecoration);
  },

  testSetStyleMap() {
    const el = $('span6');

    const styles = {
      'background-color': 'blue',
      'font-size': '100px',
      textAlign: 'center',
    };

    googStyle.setStyle(el, styles);

    const answers = {
      backgroundColor: 'blue',
      fontSize: '100px',
      textAlign: 'center',
    };

    googObject.forEach(answers, (value, style) => {
      assertEquals(`Should be ${value}`, value, el.style[style]);
    });
  },

  testSetStyleWithNonCamelizedString() {
    const el = $('span5');
    googStyle.setStyle(el, 'text-decoration', 'underline');
    assertEquals('Should be underline', 'underline', el.style.textDecoration);
  },

  testGetStyle() {
    const el = googDom.getElement('styleTest3');
    googStyle.setStyle(el, 'width', '80px');
    googStyle.setStyle(el, 'textDecoration', 'underline');

    assertEquals('80px', googStyle.getStyle(el, 'width'));
    assertEquals('underline', googStyle.getStyle(el, 'textDecoration'));
    assertEquals('underline', googStyle.getStyle(el, 'text-decoration'));
    // Non set properties are always empty strings.
    assertEquals('', googStyle.getStyle(el, 'border'));
  },

  testGetStyleMsFilter() {
    // Element with -ms-filter style set.
    const e = googDom.getElement('msFilter');

    if (userAgent.IE && userAgent.isDocumentModeOrHigher(8) &&
        !userAgent.isDocumentModeOrHigher(10)) {
      // Only IE8/9 supports -ms-filter and returns it as value for the "filter"
      // property. When in compatibility mode, -ms-filter is not supported
      // and IE8 behaves as IE7 so the other case will apply.
      assertEquals('alpha(opacity=0)', googStyle.getStyle(e, 'filter'));
    } else {
      // Any other browser does not support ms-filter so it returns empty
      // string.
      assertEquals('', googStyle.getStyle(e, 'filter'));
    }
  },

  testGetStyleFilter() {
    // Element with filter style set.
    const e = googDom.getElement('filter');

    if (userAgent.IE && !userAgent.isDocumentModeOrHigher(10)) {
      // Filter supported.
      assertEquals('alpha(opacity=0)', googStyle.getStyle(e, 'filter'));
    } else {
      assertEquals('', googStyle.getStyle(e, 'filter'));
    }
  },

  testGetComputedStyleMsFilter() {
    // Element with -ms-filter style set.
    const e = googDom.getElement('msFilter');

    if (userAgent.IE && !userAgent.isDocumentModeOrHigher(10)) {
      if (userAgent.isDocumentModeOrHigher(9)) {
        // IE 9 returns the value.
        assertEquals(
            'alpha(opacity=0)', googStyle.getComputedStyle(e, 'filter'));
      } else {
        // Older IE always returns empty string for computed styles.
        assertEquals('', googStyle.getComputedStyle(e, 'filter'));
      }
    } else {
      // Non IE returns 'none' for filter as it is an SVG property
      assertEquals('none', googStyle.getComputedStyle(e, 'filter'));
    }
  },

  testGetComputedStyleFilter() {
    // Element with filter style set.
    const e = googDom.getElement('filter');

    if (userAgent.IE && !userAgent.isDocumentModeOrHigher(10)) {
      if (userAgent.isDocumentModeOrHigher(9)) {
        // IE 9 returns the value.
        assertEquals(
            'alpha(opacity=0)', googStyle.getComputedStyle(e, 'filter'));
      } else {
        // Older IE always returns empty string for computed styles.
        assertEquals('', googStyle.getComputedStyle(e, 'filter'));
      }
    } else {
      // Non IE returns 'none' for filter as it is an SVG property
      assertEquals('none', googStyle.getComputedStyle(e, 'filter'));
    }
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testGetComputedBoxSizing() {
    const defaultBoxSizing =
        googDom.isCss1CompatMode() ? 'content-box' : 'border-box';
    let el = googDom.getElement('box-sizing-unset');
    assertEquals(defaultBoxSizing, googStyle.getComputedBoxSizing(el));

    el = googDom.getElement('box-sizing-border-box');
    assertEquals('border-box', googStyle.getComputedBoxSizing(el));
  },

  testGetComputedPosition() {
    assertEquals(
        'position not set', 'static',
        googStyle.getComputedPosition($('position-unset')));
    assertEquals(
        'position:relative in style attribute', 'relative',
        googStyle.getComputedPosition($('style-position-relative')));
    assertEquals(
        'position:fixed in style attribute', 'fixed',
        googStyle.getComputedPosition($('style-position-fixed')));
    assertEquals(
        'position:absolute in css', 'absolute',
        googStyle.getComputedPosition($('css-position-absolute')));
  },

  testGetComputedOverflowXAndY() {
    assertEquals(
        'overflow-x:scroll in style attribute', 'scroll',
        googStyle.getComputedOverflowX($('style-overflow-scroll')));
    assertEquals(
        'overflow-y:scroll in style attribute', 'scroll',
        googStyle.getComputedOverflowY($('style-overflow-scroll')));
    assertEquals(
        'overflow-x:hidden in css', 'hidden',
        googStyle.getComputedOverflowX($('css-overflow-hidden')));
    assertEquals(
        'overflow-y:hidden in css', 'hidden',
        googStyle.getComputedOverflowY($('css-overflow-hidden')));
  },

  testGetComputedZIndex() {
    assertEquals(
        'z-index:200 in style attribute', '200',
        '' + googStyle.getComputedZIndex($('style-z-index-200')));
    assertEquals(
        'z-index:200 in css', '200',
        '' + googStyle.getComputedZIndex($('css-z-index-200')));
  },

  testGetComputedTextAlign() {
    assertEquals(
        'text-align:right in style attribute', 'right',
        googStyle.getComputedTextAlign($('style-text-align-right')));
    assertEquals(
        'text-align:right inherited from parent', 'right',
        googStyle.getComputedTextAlign($('style-text-align-right-inner')));
    assertEquals(
        'text-align:center in css', 'center',
        googStyle.getComputedTextAlign($('css-text-align-center')));
  },

  testGetComputedCursor() {
    assertEquals(
        'cursor:move in style attribute', 'move',
        googStyle.getComputedCursor($('style-cursor-move')));
    assertEquals(
        'cursor:move inherited from parent', 'move',
        googStyle.getComputedCursor($('style-cursor-move-inner')));
    assertEquals(
        'cursor:poiner in css', 'pointer',
        googStyle.getComputedCursor($('css-cursor-pointer')));
  },

  testGetBackgroundColor() {
    const dest = $('bgcolorDest');

    for (let i = 0; $(`bgcolorTest${i}`); i++) {
      const src = $(`bgcolorTest${i}`);
      const bgColor = googStyle.getBackgroundColor(src);

      dest.style.backgroundColor = bgColor;
      assertEquals(
          'Background colors should be equal',
          googStyle.getBackgroundColor(src),
          googStyle.getBackgroundColor(dest));

      let c = null;
      try {
        // goog.color.parse throws a generic exception if handed input it
        // doesn't understand.
        c = color.parse(bgColor);
      } catch (e) {
        // Internet Explorer is unable to parse colors correctly after test 4.
        // Other browsers may vary, but all should be able to handle straight
        // hex input.
        assertFalse(`Should be able to parse color "${bgColor}"`, i < 5);
        break;
      }
      if (i <= 5) {
        assertEquals('parse test', 'rgb(255,0,0)', color.hexToRgbStyle(c.hex));
      }
    }
  },

  testSetPosition() {
    const el = $('testEl');

    googStyle.setPosition(el, 100, 100);
    assertEquals('100px', el.style.left);
    assertEquals('100px', el.style.top);

    googStyle.setPosition(el, '50px', '25px');
    assertEquals('50px', el.style.left);
    assertEquals('25px', el.style.top);

    googStyle.setPosition(el, '10ex', '25px');
    assertEquals('10ex', el.style.left);
    assertEquals('25px', el.style.top);

    googStyle.setPosition(el, '10%', '25%');
    assertEquals('10%', el.style.left);
    assertEquals('25%', el.style.top);

    // ignores bad units
    googStyle.setPosition(el, 0, 0);
    googStyle.setPosition(el, '10rainbows', '25rainbows');
    assertEquals('0px', el.style.left);
    assertEquals('0px', el.style.top);

    googStyle.setPosition(el, new Coordinate(20, 40));
    assertEquals('20px', el.style.left);
    assertEquals('40px', el.style.top);
  },

  testGetClientPositionAbsPositionElement() {
    const div = googDom.createDom(TagName.DIV);
    div.style.position = 'absolute';
    div.style.left = '100px';
    div.style.top = '200px';
    document.body.appendChild(div);
    const pos = googStyle.getClientPosition(div);
    assertEquals(100, pos.x);
    assertEquals(200, pos.y);
  },

  testGetClientPositionNestedElements() {
    const innerDiv = googDom.createDom(TagName.DIV);
    innerDiv.style.position = 'relative';
    innerDiv.style.left = '-10px';
    innerDiv.style.top = '-10px';
    const div = googDom.createDom(TagName.DIV);
    div.style.position = 'absolute';
    div.style.left = '150px';
    div.style.top = '250px';
    div.appendChild(innerDiv);
    document.body.appendChild(div);
    const pos = googStyle.getClientPosition(innerDiv);
    assertEquals(140, pos.x);
    assertEquals(240, pos.y);
  },

  testGetClientPositionOfOffscreenElement() {
    const div = googDom.createDom(TagName.DIV);
    div.style.position = 'absolute';
    div.style.left = '2000px';
    div.style.top = '2000px';
    div.style.width = '10px';
    div.style.height = '10px';
    document.body.appendChild(div);

    try {
      window.scroll(0, 0);
      let pos = googStyle.getClientPosition(div);
      assertEquals(2000, pos.x);
      assertEquals(2000, pos.y);

      // The following tests do not work in IE, due to an
      // obscure off-by-one bug in goog.style.getPageOffset.
      if (!userAgent.IE) {
        window.scroll(1, 1);
        let pos = googStyle.getClientPosition(div);
        assertEquals(1999, pos.x);
        assertRoughlyEquals(1999, pos.y, 0.5);

        window.scroll(2, 2);
        pos = googStyle.getClientPosition(div);
        assertEquals(1998, pos.x);
        assertRoughlyEquals(1998, pos.y, 0.5);

        window.scroll(100, 100);
        pos = googStyle.getClientPosition(div);
        assertEquals(1900, pos.x);
        assertRoughlyEquals(1900, pos.y, 0.5);
      }
    } finally {
      window.scroll(0, 0);
      document.body.removeChild(div);
    }
  },

  testGetClientPositionOfOrphanElement() {
    const orphanElem = googDom.createElement(TagName.DIV);
    const pos = googStyle.getClientPosition(orphanElem);
    assertEquals(0, pos.x);
    assertEquals(0, pos.y);
  },

  testGetClientPositionEvent() {
    const mockEvent = {};
    mockEvent.clientX = 100;
    mockEvent.clientY = 200;
    /** @suppress {checkTypes} suppression added to enable type checking */
    const pos = googStyle.getClientPosition(mockEvent);
    assertEquals(100, pos.x);
    assertEquals(200, pos.y);
  },

  testGetClientPositionTouchEvent() {
    const mockTouchEvent = {};
    mockTouchEvent.changedTouches = [{}];
    mockTouchEvent.changedTouches[0].clientX = 100;
    mockTouchEvent.changedTouches[0].clientY = 200;

    /** @suppress {checkTypes} suppression added to enable type checking */
    const pos = googStyle.getClientPosition(mockTouchEvent);
    assertEquals(100, pos.x);
    assertEquals(200, pos.y);
  },

  testGetClientPositionEmptyTouchList() {
    const mockTouchEvent = {};

    mockTouchEvent.touches = [];

    mockTouchEvent.changedTouches = [{}];
    mockTouchEvent.changedTouches[0].clientX = 100;
    mockTouchEvent.changedTouches[0].clientY = 200;

    /** @suppress {checkTypes} suppression added to enable type checking */
    const pos = googStyle.getClientPosition(mockTouchEvent);
    assertEquals(100, pos.x);
    assertEquals(200, pos.y);
  },

  testGetClientPositionAbstractedTouchEvent() {
    const mockTouchEvent = {};
    mockTouchEvent.changedTouches = [{}];
    mockTouchEvent.changedTouches[0].clientX = 100;
    mockTouchEvent.changedTouches[0].clientY = 200;

    /** @suppress {checkTypes} suppression added to enable type checking */
    const e = new BrowserEvent(mockTouchEvent);

    const pos = googStyle.getClientPosition(e);
    assertEquals(100, pos.x);
    assertEquals(200, pos.y);
  },

  testGetPageOffsetAbsPositionedElement() {
    const div = googDom.createDom(TagName.DIV);
    div.style.position = 'absolute';
    div.style.left = '100px';
    div.style.top = '200px';
    document.body.appendChild(div);
    const pos = googStyle.getPageOffset(div);
    assertEquals(100, pos.x);
    assertEquals(200, pos.y);
  },

  testGetPageOffsetNestedElements() {
    const innerDiv = googDom.createDom(TagName.DIV);
    innerDiv.style.position = 'relative';
    innerDiv.style.left = '-10px';
    innerDiv.style.top = '-10px';
    const div = googDom.createDom(TagName.DIV);
    div.style.position = 'absolute';
    div.style.left = '150px';
    div.style.top = '250px';
    div.appendChild(innerDiv);
    document.body.appendChild(div);
    const pos = googStyle.getPageOffset(innerDiv);
    assertRoughlyEquals(140, pos.x, 0.1);
    assertRoughlyEquals(240, pos.y, 0.1);
  },

  testGetPageOffsetWithBodyPadding() {
    document.body.style.margin = '40px';
    document.body.style.padding = '60px';
    document.body.style.borderWidth = '70px';
    let div;
    try {
      div = googDom.createDom(TagName.DIV);
      div.style.position = 'absolute';
      div.style.left = '100px';
      div.style.top = '200px';
      // Margin will affect position, but padding and borders should not.
      div.style.margin = '1px';
      div.style.padding = '2px';
      div.style.borderWidth = '3px';
      document.body.appendChild(div);
      const pos = googStyle.getPageOffset(div);
      assertRoughlyEquals(101, pos.x, 0.1);
      assertRoughlyEquals(201, pos.y, 0.1);
    } finally {
      document.body.removeChild(div);
      document.body.style.margin = '';
      document.body.style.padding = '';
      document.body.style.borderWidth = '';
    }
  },

  testGetPageOffsetWithDocumentElementPadding() {
    document.documentElement.style.margin = '40px';
    document.documentElement.style.padding = '60px';
    document.documentElement.style.borderWidth = '70px';
    let div;
    try {
      div = googDom.createDom(TagName.DIV);
      div.style.position = 'absolute';
      div.style.left = '100px';
      div.style.top = '200px';
      // Margin will affect position, but padding and borders should not.
      div.style.margin = '1px';
      div.style.padding = '2px';
      div.style.borderWidth = '3px';
      document.body.appendChild(div);
      const pos = googStyle.getPageOffset(div);
      assertRoughlyEquals(101, pos.x, 0.1);
      assertRoughlyEquals(201, pos.y, 0.1);
    } finally {
      document.body.removeChild(div);
      document.documentElement.style.margin = '';
      document.documentElement.style.padding = '';
      document.documentElement.style.borderWidth = '';
    }
  },

  testGetPageOffsetElementOffscreen() {
    const div = googDom.createDom(TagName.DIV);
    div.style.position = 'absolute';
    div.style.left = '10000px';
    div.style.top = '20000px';
    document.body.appendChild(div);
    window.scroll(0, 0);
    try {
      let pos = googStyle.getPageOffset(div);
      assertEquals(10000, pos.x);
      assertEquals(20000, pos.y);

      // The following tests do not work in IE, due to an
      // obscure off-by-one bug in goog.style.getPageOffset.
      if (!userAgent.IE) {
        window.scroll(1, 1);
        pos = googStyle.getPageOffset(div);
        assertEquals(10000, pos.x);
        assertRoughlyEquals(20000, pos.y, 0.5);

        window.scroll(1000, 2000);
        pos = googStyle.getPageOffset(div);
        assertEquals(10000, pos.x);
        assertRoughlyEquals(20000, pos.y, 1);

        window.scroll(10000, 20000);
        pos = googStyle.getPageOffset(div);
        assertEquals(10000, pos.x);
        assertRoughlyEquals(20000, pos.y, 1);
      }
    }
    // Undo changes.
    finally {
      document.body.removeChild(div);
      window.scroll(0, 0);
    }
  },

  testGetPageOffsetFixedPositionElements() {
    // Skip these tests in certain browsers.
    // position:fixed is not supported in IE before version 7
    if (!userAgent.IE) {
      // Test with a position fixed element
      let div = googDom.createDom(TagName.DIV);
      div.style.position = 'fixed';
      div.style.top = '10px';
      div.style.left = '10px';
      document.body.appendChild(div);
      let pos = googStyle.getPageOffset(div);
      assertEquals(10, pos.x);
      assertEquals(10, pos.y);

      // Test with a position fixed element as parent
      const innerDiv = googDom.createDom(TagName.DIV);
      div = googDom.createDom(TagName.DIV);
      div.style.position = 'fixed';
      div.style.top = '10px';
      div.style.left = '10px';
      div.style.padding = '5px';
      div.appendChild(innerDiv);
      document.body.appendChild(div);
      pos = googStyle.getPageOffset(innerDiv);
      assertEquals(15, pos.x);
      assertEquals(15, pos.y);
    }
  },

  testGetPositionTolerantToNoDocumentElementBorder() {
    // In IE, removing the border on the document element undoes the normal
    // 2-pixel offset.  Ensure that we're correctly compensating for both cases.
    try {
      document.documentElement.style.borderWidth = '0';
      const div = googDom.createDom(TagName.DIV);
      div.style.position = 'absolute';
      div.style.left = '100px';
      div.style.top = '200px';
      document.body.appendChild(div);

      // Test all major positioning methods.

      try {
        // Test all major positioning methods.
        const pos = googStyle.getClientPosition(div);
        assertEquals(100, pos.x);
        assertRoughlyEquals(200, pos.y, .1);
        const offset = googStyle.getPageOffset(div);
        assertEquals(100, offset.x);
        assertRoughlyEquals(200, offset.y, .1);
      } catch (e) {
        expectedFailures.handleException(e);
      }
    } finally {
      document.documentElement.style.borderWidth = '';
    }
  },

  testSetSize() {
    const el = $('testEl');

    googStyle.setSize(el, 100, 100);
    assertEquals('100px', el.style.width);
    assertEquals('100px', el.style.height);

    googStyle.setSize(el, '50px', '25px');
    assertEquals('should be "50px"', '50px', el.style.width);
    assertEquals('should be "25px"', '25px', el.style.height);

    googStyle.setSize(el, '10ex', '25px');
    assertEquals('10ex', el.style.width);
    assertEquals('25px', el.style.height);

    googStyle.setSize(el, '10%', '25%');
    assertEquals('10%', el.style.width);
    assertEquals('25%', el.style.height);

    // ignores bad units
    googStyle.setSize(el, 0, 0);
    googStyle.setSize(el, '10rainbows', '25rainbows');
    assertEquals('0px', el.style.width);
    assertEquals('0px', el.style.height);

    googStyle.setSize(el, new Size(20, 40));
    assertEquals('20px', el.style.width);
    assertEquals('40px', el.style.height);
  },

  testSetWidthAndHeight() {
    const el = $('testEl');

    // Replicate all of the setSize tests above.

    googStyle.setWidth(el, 100);
    googStyle.setHeight(el, 100);
    assertEquals('100px', el.style.width);
    assertEquals('100px', el.style.height);

    googStyle.setWidth(el, '50px');
    googStyle.setHeight(el, '25px');
    assertEquals('should be "50px"', '50px', el.style.width);
    assertEquals('should be "25px"', '25px', el.style.height);

    googStyle.setWidth(el, '10ex');
    googStyle.setHeight(el, '25px');
    assertEquals('10ex', el.style.width);
    assertEquals('25px', el.style.height);

    googStyle.setWidth(el, '10%');
    googStyle.setHeight(el, '25%');
    assertEquals('10%', el.style.width);
    assertEquals('25%', el.style.height);

    googStyle.setWidth(el, 0);
    googStyle.setHeight(el, 0);
    assertEquals('0px', el.style.width);
    assertEquals('0px', el.style.height);

    googStyle.setWidth(el, 20);
    googStyle.setHeight(el, 40);
    assertEquals('20px', el.style.width);
    assertEquals('40px', el.style.height);

    // Additional tests testing each separately.
    googStyle.setWidth(el, '');
    googStyle.setHeight(el, '');
    assertEquals('', el.style.width);
    assertEquals('', el.style.height);

    googStyle.setHeight(el, 20);
    assertEquals('', el.style.width);
    assertEquals('20px', el.style.height);

    googStyle.setWidth(el, 40);
    assertEquals('40px', el.style.width);
    assertEquals('20px', el.style.height);
  },

  testGetSize() {
    let el = $('testEl');
    googStyle.setSize(el, 100, 100);

    let dims = googStyle.getSize(el);
    assertEquals(100, dims.width);
    assertEquals(100, dims.height);

    googStyle.setStyle(el, 'display', 'none');
    dims = googStyle.getSize(el);
    assertEquals(100, dims.width);
    assertEquals(100, dims.height);

    el = $('testEl5');
    googStyle.setSize(el, 100, 100);
    dims = googStyle.getSize(el);
    assertEquals(100, dims.width);
    assertEquals(100, dims.height);

    el = $('span0');
    dims = googStyle.getSize(el);
    assertNotEquals(0, dims.width);
    assertNotEquals(0, dims.height);

    el = $('table1');
    dims = googStyle.getSize(el);
    assertNotEquals(0, dims.width);
    assertNotEquals(0, dims.height);

    el = $('td1');
    dims = googStyle.getSize(el);
    assertNotEquals(0, dims.width);
    assertNotEquals(0, dims.height);

    el = $('li1');
    dims = googStyle.getSize(el);
    assertNotEquals(0, dims.width);
    assertNotEquals(0, dims.height);

    el = googDom.getElementsByTagNameAndClass(TagName.HTML)[0];
    dims = googStyle.getSize(el);
    assertNotEquals(0, dims.width);
    assertNotEquals(0, dims.height);

    el = googDom.getElementsByTagNameAndClass(TagName.BODY)[0];
    dims = googStyle.getSize(el);
    assertNotEquals(0, dims.width);
    assertNotEquals(0, dims.height);
  },

  testGetSizeSvgElements() {
    const svgEl = document.createElementNS &&
        document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    if (!svgEl || svgEl.getAttribute('transform') == '' ||
        (userAgent.WEBKIT && !userAgent.isVersionOrHigher(534.8))) {
      // SVG not supported, or getBoundingClientRect not supported on SVG
      // elements.
      return;
    }

    document.body.appendChild(svgEl);
    const el = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    el.setAttribute('x', 10);
    el.setAttribute('y', 10);
    el.setAttribute('width', 32);
    el.setAttribute('height', 21);
    el.setAttribute('fill', '#000');

    svgEl.appendChild(el);

    // The bounding size in 1 larger than the SVG element in IE.
    const expectedWidth = (userAgent.EDGE_OR_IE) ? 33 : 32;
    const expectedHeight = (userAgent.EDGE_OR_IE) ? 22 : 21;

    let dims = googStyle.getSize(el);
    assertEquals(expectedWidth, dims.width);
    assertRoughlyEquals(expectedHeight, dims.height, 0.01);

    dims = googStyle.getSize(svgEl);
    // The size of the <svg> will be the viewport size on all browsers. This
    // used to not be true for Firefox, but they fixed the glitch in Firefox 33.
    // https://bugzilla.mozilla.org/show_bug.cgi?id=530985
    assertTrue(dims.width >= expectedWidth);
    assertTrue(dims.height >= expectedHeight);

    el.style.visibility = 'none';

    dims = googStyle.getSize(el);
    assertEquals(expectedWidth, dims.width);
    assertRoughlyEquals(expectedHeight, dims.height, 0.01);

    dims = googStyle.getSize(svgEl);
    assertTrue(dims.width >= expectedWidth);
    assertTrue(dims.height >= expectedHeight);
  },

  testGetSizeSvgDocument() {
    const svgEl = document.createElementNS &&
        document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    if (!svgEl || svgEl.getAttribute('transform') == '' ||
        (userAgent.WEBKIT && !userAgent.isVersionOrHigher(534.8))) {
      // SVG not supported, or getBoundingClientRect not supported on SVG
      // elements.
      return;
    }

    const frame = googDom.getElement('svg-frame');
    const doc = googDom.getFrameContentDocument(frame);
    const rect = doc.getElementById('rect');
    const dims = googStyle.getSize(rect);
    if (userAgent.GECKO && userAgent.isVersionOrHigher(53) &&
        !userAgent.isVersionOrHigher(68)) {
      // Firefox >= 53 < 68 auto-scales iframe SVG content to fit the frame
      // b/38432885 | https://bugzilla.mozilla.org/show_bug.cgi?id=1366126
      assertEquals(75, dims.width);
      assertEquals(75, dims.height);
    } else if (!userAgent.EDGE_OR_IE) {
      assertEquals(50, dims.width);
      assertEquals(50, dims.height);
    } else {
      assertEquals(51, dims.width);
      assertEquals(51, dims.height);
    }
  },

  testGetSizeInlineBlock() {
    const el = $('height-test-inner');
    const dims = googStyle.getSize(el);
    assertNotEquals(0, dims.height);
  },

  testGetSizeTransformedRotated() {
    if (!hasWebkitTransform()) return;

    const el = $('rotated');
    googStyle.setSize(el, 300, 200);

    /** @suppress {checkTypes} suppression added to enable type checking */
    const noRotateDims = googStyle.getTransformedSize(el);
    assertEquals(300, noRotateDims.width);
    assertEquals(200, noRotateDims.height);

    el.style.webkitTransform = 'rotate(180deg)';
    const rotate180Dims = googStyle.getTransformedSize(el);
    assertEquals(300, rotate180Dims.width);
    assertEquals(200, rotate180Dims.height);

    el.style.webkitTransform = 'rotate(90deg)';
    const rotate90ClockwiseDims = googStyle.getTransformedSize(el);
    assertEquals(200, rotate90ClockwiseDims.width);
    assertEquals(300, rotate90ClockwiseDims.height);

    el.style.webkitTransform = 'rotate(-90deg)';
    const rotate90CounterClockwiseDims = googStyle.getTransformedSize(el);
    assertEquals(200, rotate90CounterClockwiseDims.width);
    assertEquals(300, rotate90CounterClockwiseDims.height);
  },

  testGetSizeTransformedScaled() {
    if (!hasWebkitTransform()) return;

    const el = $('scaled');
    googStyle.setSize(el, 300, 200);

    /** @suppress {checkTypes} suppression added to enable type checking */
    const noScaleDims = googStyle.getTransformedSize(el);
    assertEquals(300, noScaleDims.width);
    assertEquals(200, noScaleDims.height);

    el.style.webkitTransform = 'scale(2, 0.5)';
    const scaledDims = googStyle.getTransformedSize(el);
    assertEquals(600, scaledDims.width);
    assertEquals(100, scaledDims.height);
  },

  testGetSizeOfOrphanElement() {
    const orphanElem = googDom.createElement(TagName.DIV);
    const size = googStyle.getSize(orphanElem);
    assertEquals(0, size.width);
    assertEquals(0, size.height);
  },

  testGetBounds() {
    const el = $('testEl');

    const dims = googStyle.getSize(el);
    const pos = googStyle.getPageOffset(el);

    const rect = googStyle.getBounds(el);

    // Relies on getSize and getPageOffset being correct.
    assertEquals(dims.width, rect.width);
    assertEquals(dims.height, rect.height);
    assertEquals(pos.x, rect.left);
    assertEquals(pos.y, rect.top);
  },

  testInstallSafeStyleSheet() {
    const el = $('installTest0');
    const originalBackground = googStyle.getBackgroundColor(el);

    // Uses background-color because it's easy to get the computed value
    const result =
        googStyle.installSafeStyleSheet(testing.newSafeStyleSheetForTest(
            '#installTest0 { background-color: rgb(255, 192, 203); }'));

    assertColorRgbEquals('rgb(255,192,203)', googStyle.getBackgroundColor(el));

    googStyle.uninstallStyles(result);
    assertEquals(originalBackground, googStyle.getBackgroundColor(el));
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testInstallSafeStyleSheetWithNonce() {
    if (isSafari) {
      // Safari has a quirk where dynamically installed stylesheets lose
      // their nonce attribute. This causes this test to fail, but is
      // not a reliability concern (since we don't yet use CSP to restrict
      // CSS loading).
      return;
    }
    const result =
        googStyle.installSafeStyleSheet(testing.newSafeStyleSheetForTest(''));

    const styles = document.head.querySelectorAll('style[nonce]');
    assert(styles.length > 1);
    const lastStyleSheet = styles[styles.length - 1];
    // Some browsers don't expose the nonce via getAttribute so try and retrieve
    // it as a normal property first.
    assertEquals(
        'NONCE', /** @type {?} */ (lastStyleSheet).nonce ||
            lastStyleSheet.getAttribute('nonce'));

    googStyle.uninstallStyles(result);
  },

  testSetSafeStyleSheet() {
    const el = $('installTest1');

    // Change to pink
    const ss = googStyle.installSafeStyleSheet(testing.newSafeStyleSheetForTest(
        '#installTest1 { background-color: rgb(255, 192, 203); }'));

    assertColorRgbEquals('rgb(255,192,203)', googStyle.getBackgroundColor(el));

    // Now change to orange
    googStyle.setSafeStyleSheet(
        ss,
        testing.newSafeStyleSheetForTest(
            '#installTest1 { background-color: rgb(255, 255, 0); }'));
    assertColorRgbEquals('rgb(255,255,0)', googStyle.getBackgroundColor(el));
  },

  testIsRightToLeft() {
    assertFalse(googStyle.isRightToLeft($('rtl1')));
    assertTrue(googStyle.isRightToLeft($('rtl2')));
    assertFalse(googStyle.isRightToLeft($('rtl3')));
    assertFalse(googStyle.isRightToLeft($('rtl4')));
    assertTrue(googStyle.isRightToLeft($('rtl5')));
    assertFalse(googStyle.isRightToLeft($('rtl6')));
    assertTrue(googStyle.isRightToLeft($('rtl7')));
    assertFalse(googStyle.isRightToLeft($('rtl8')));
    assertTrue(googStyle.isRightToLeft($('rtl9')));
    assertFalse(googStyle.isRightToLeft($('rtl10')));
  },

  testIsUnselectable() {
    assertEquals(
        userAgent.GECKO, googStyle.isUnselectable($('unselectable-gecko')));
    assertEquals(userAgent.IE, googStyle.isUnselectable($('unselectable-ie')));
    // Note: Firefox can go either way here - newer versions see -webkit-*
    // properties and automatically add Moz* to the style object.
    if (!userAgent.GECKO) {
      assertEquals(
          userAgent.WEBKIT || userAgent.EDGE,
          googStyle.isUnselectable($('unselectable-webkit')));
    }
  },

  testSetUnselectable() {
    const el = $('make-unselectable');
    assertFalse(googStyle.isUnselectable(el));

    function assertDescendantsUnselectable(unselectable) {
      Array.prototype.forEach.call(el.getElementsByTagName('*'), descendant => {
        // Skip MathML or any other elements that do not have a style property.
        if (descendant.style) {
          assertEquals(unselectable, googStyle.isUnselectable(descendant));
        }
      });
    }

    googStyle.setUnselectable(el, true);
    assertTrue(googStyle.isUnselectable(el));
    assertDescendantsUnselectable(true);

    googStyle.setUnselectable(el, false);
    assertFalse(googStyle.isUnselectable(el));
    assertDescendantsUnselectable(false);

    googStyle.setUnselectable(el, true, true);
    assertTrue(googStyle.isUnselectable(el));
    assertDescendantsUnselectable(false);

    googStyle.setUnselectable(el, false, true);
    assertFalse(googStyle.isUnselectable(el));
    assertDescendantsUnselectable(false);
  },

  testPosWithAbsoluteAndScroll() {
    const el = $('pos-scroll-abs');
    const el1 = $('pos-scroll-abs-1');
    const el2 = $('pos-scroll-abs-2');

    el1.scrollTop = 200;
    const pos = googStyle.getPageOffset(el2);

    assertEquals(200, pos.x);
    // Don't bother with IE in quirks mode
    if (!userAgent.IE || document.compatMode == 'CSS1Compat') {
      assertRoughlyEquals(300, pos.y, .1);
    }
  },

  testPosWithAbsoluteAndWindowScroll() {
    window.scrollBy(0, 200);
    const el = $('abs-upper-left');
    const pos = googStyle.getPageOffset(el);
    assertRoughlyEquals('Top should be about 0', 0, pos.y, 0.1);
  },

  testGetBorderBoxSize() {
    // Strict mode
    const getBorderBoxSize = googStyle.getBorderBoxSize;

    let el = $('size-a');
    let rect = getBorderBoxSize(el);
    assertEquals('width:100px', 100, rect.width);
    assertEquals('height:100px', 100, rect.height);

    // with border: 10px
    el = $('size-b');
    rect = getBorderBoxSize(el);
    assertEquals(
        'width:100px;border:10px', isBorderBox ? 100 : 120, rect.width);
    assertEquals(
        'height:100px;border:10px', isBorderBox ? 100 : 120, rect.height);

    // with border: 10px; padding: 10px
    el = $('size-c');
    rect = getBorderBoxSize(el);
    assertEquals(
        'width:100px;border:10px;padding:10px', isBorderBox ? 100 : 140,
        rect.width);
    assertEquals(
        'height:100px;border:10px;padding:10px', isBorderBox ? 100 : 140,
        rect.height);

    // size, padding and borders are all in non pixel units
    // all we test here is that we get a number out
    el = $('size-d');
    rect = getBorderBoxSize(el);
    assertEquals('number', typeof rect.width);
    assertEquals('number', typeof rect.height);
    assertFalse(isNaN(rect.width));
    assertFalse(isNaN(rect.height));
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testGetContentBoxSize() {
    // Strict mode
    const getContentBoxSize = googStyle.getContentBoxSize;

    let el = $('size-a');
    let rect = getContentBoxSize(el);
    assertEquals('width:100px', 100, rect.width);
    assertEquals('height:100px', 100, rect.height);

    // with border: 10px
    el = $('size-b');
    rect = getContentBoxSize(el);
    assertEquals('width:100px;border:10px', isBorderBox ? 80 : 100, rect.width);
    assertEquals(
        'height:100px;border:10px', isBorderBox ? 80 : 100, rect.height);

    // with border: 10px; padding: 10px
    el = $('size-c');
    rect = getContentBoxSize(el);
    assertEquals(
        'width:100px;border:10px;padding:10px', isBorderBox ? 60 : 100,
        rect.width);
    assertEquals(
        'height:100px;border:10px;padding:10px', isBorderBox ? 60 : 100,
        rect.height);

    // size, padding and borders are all in non pixel units
    // all we test here is that we get a number out
    el = $('size-d');
    rect = getContentBoxSize(el);
    assertEquals('number', typeof rect.width);
    assertEquals('number', typeof rect.height);
    assertFalse(isNaN(rect.width));
    assertFalse(isNaN(rect.height));

    // test whether getContentBoxSize works when width and height
    // aren't explicitly set, but the default of 'auto'.
    // 'size-f' has no margin, border, or padding, so offsetWidth/Height
    // should match the content box size
    el = $('size-f');
    rect = getContentBoxSize(el);
    assertEquals(el.offsetWidth, rect.width);
    assertEquals(el.offsetHeight, rect.height);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testSetBorderBoxSize() {
    // Strict mode
    const el = $('size-e');
    const setBorderBoxSize = googStyle.setBorderBoxSize;

    // Clean up
    // style element has 100x100, no border and no padding
    el.style.padding = '';
    el.style.margin = '';
    el.style.borderWidth = '';
    el.style.width = '';
    el.style.height = '';

    setBorderBoxSize(el, new Size(100, 100));

    assertEquals(100, el.offsetWidth);
    assertEquals(100, el.offsetHeight);

    el.style.borderWidth = '10px';
    setBorderBoxSize(el, new Size(100, 100));

    assertEquals('width:100px;border:10px', 100, el.offsetWidth);
    assertEquals('height:100px;border:10px', 100, el.offsetHeight);

    el.style.padding = '10px';
    setBorderBoxSize(el, new Size(100, 100));
    assertEquals(100, el.offsetWidth);
    assertEquals(100, el.offsetHeight);

    el.style.borderWidth = '0';
    setBorderBoxSize(el, new Size(100, 100));
    assertEquals(100, el.offsetWidth);
    assertEquals(100, el.offsetHeight);

    if (userAgent.GECKO) {
      assertEquals('border-box', el.style.MozBoxSizing);
    } else if (userAgent.WEBKIT) {
      assertEquals('border-box', el.style.WebkitBoxSizing);
    } else if (
        userAgent.IE && userAgent.isDocumentModeOrHigher(8)) {
      assertEquals('border-box', el.style.boxSizing);
    }

    // Try a negative width/height.
    setBorderBoxSize(el, new Size(-10, -10));

    // Setting the border box smaller than the borders will just give you
    // a content box of size 0.
    // NOTE(nicksantos): I'm not really sure why IE7 is special here.
    const isIeLt8Quirks = userAgent.IE &&
        !userAgent.isDocumentModeOrHigher(8) && !googDom.isCss1CompatMode();
    assertEquals(20, el.offsetWidth);
    assertEquals(isIeLt8Quirks ? 39 : 20, el.offsetHeight);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testSetContentBoxSize() {
    // Strict mode
    const el = $('size-e');
    const setContentBoxSize = googStyle.setContentBoxSize;

    // Clean up
    // style element has 100x100, no border and no padding
    el.style.padding = '';
    el.style.margin = '';
    el.style.borderWidth = '';
    el.style.width = '';
    el.style.height = '';

    setContentBoxSize(el, new Size(100, 100));

    assertEquals(100, el.offsetWidth);
    assertEquals(100, el.offsetHeight);

    el.style.borderWidth = '10px';
    setContentBoxSize(el, new Size(100, 100));
    assertEquals('width:100px;border-width:10px', 120, el.offsetWidth);
    assertEquals('height:100px;border-width:10px', 120, el.offsetHeight);

    el.style.padding = '10px';
    setContentBoxSize(el, new Size(100, 100));
    assertEquals(
        'width:100px;border-width:10px;padding:10px', 140, el.offsetWidth);
    assertEquals(
        'height:100px;border-width:10px;padding:10px', 140, el.offsetHeight);

    el.style.borderWidth = '0';
    setContentBoxSize(el, new Size(100, 100));
    assertEquals('width:100px;padding:10px', 120, el.offsetWidth);
    assertEquals('height:100px;padding:10px', 120, el.offsetHeight);

    if (userAgent.GECKO) {
      assertEquals('content-box', el.style.MozBoxSizing);
    } else if (userAgent.WEBKIT) {
      assertEquals('content-box', el.style.WebkitBoxSizing);
    } else if (
        userAgent.IE && userAgent.isDocumentModeOrHigher(8)) {
      assertEquals('content-box', el.style.boxSizing);
    }

    // Try a negative width/height.
    setContentBoxSize(el, new Size(-10, -10));

    // NOTE(nicksantos): I'm not really sure why IE7 is special here.
    /** @suppress {checkTypes} suppression added to enable type checking */
    const isIeLt8Quirks = userAgent.IE &&
        !userAgent.isDocumentModeOrHigher('8') && !googDom.isCss1CompatMode();
    assertEquals(20, el.offsetWidth);
    assertEquals(isIeLt8Quirks ? 39 : 20, el.offsetHeight);
  },

  testGetPaddingBox() {
    // Strict mode
    const el = $('size-e');
    const getPaddingBox = googStyle.getPaddingBox;

    // Clean up
    // style element has 100x100, no border and no padding
    el.style.padding = '';
    el.style.margin = '';
    el.style.borderWidth = '';
    el.style.width = '';
    el.style.height = '';

    el.style.padding = '10px';
    let rect = getPaddingBox(el);
    assertEquals(10, rect.left);
    assertEquals(10, rect.right);
    assertEquals(10, rect.top);
    assertEquals(10, rect.bottom);

    el.style.padding = '0';
    rect = getPaddingBox(el);
    assertEquals(0, rect.left);
    assertEquals(0, rect.right);
    assertEquals(0, rect.top);
    assertEquals(0, rect.bottom);

    el.style.padding = '1px 2px 3px 4px';
    rect = getPaddingBox(el);
    assertEquals(1, rect.top);
    assertEquals(2, rect.right);
    assertEquals(3, rect.bottom);
    assertEquals(4, rect.left);

    el.style.padding = '1mm 2em 3ex 4%';
    rect = getPaddingBox(el);
    assertFalse(isNaN(rect.top));
    assertFalse(isNaN(rect.right));
    assertFalse(isNaN(rect.bottom));
    assertFalse(isNaN(rect.left));
    assertTrue(rect.top >= 0);
    assertTrue(rect.right >= 0);
    assertTrue(rect.bottom >= 0);
    assertTrue(rect.left >= 0);
  },

  testGetPaddingBoxUnattached() {
    const el = googDom.createElement(TagName.DIV);
    const box = googStyle.getPaddingBox(el);
    if (userAgent.WEBKIT ||
        (userAgent.GECKO && userAgent.isVersionOrHigher(64))) {
      assertTrue(isNaN(box.top));
      assertTrue(isNaN(box.right));
      assertTrue(isNaN(box.bottom));
      assertTrue(isNaN(box.left));
    } else {
      assertObjectEquals(new Box(0, 0, 0, 0), box);
    }
  },

  testGetMarginBox() {
    // Strict mode
    const el = $('size-e');
    const getMarginBox = googStyle.getMarginBox;

    // Clean up
    // style element has 100x100, no border and no padding
    el.style.padding = '';
    el.style.margin = '';
    el.style.borderWidth = '';
    el.style.width = '';
    el.style.height = '';

    el.style.margin = '10px';
    let rect = getMarginBox(el);
    assertEquals(10, rect.left);
    // In webkit the right margin is the calculated distance from right edge and
    // not the computed right margin so it is not reliable.
    // See https://bugs.webkit.org/show_bug.cgi?id=19828
    if (!userAgent.WEBKIT) {
      assertEquals(10, rect.right);
    }
    assertEquals(10, rect.top);
    assertEquals(10, rect.bottom);

    el.style.margin = '0';
    rect = getMarginBox(el);
    assertEquals(0, rect.left);
    // In webkit the right margin is the calculated distance from right edge and
    // not the computed right margin so it is not reliable.
    // See https://bugs.webkit.org/show_bug.cgi?id=19828
    if (!userAgent.WEBKIT) {
      assertEquals(0, rect.right);
    }
    assertEquals(0, rect.top);
    assertEquals(0, rect.bottom);

    el.style.margin = '1px 2px 3px 4px';
    rect = getMarginBox(el);
    assertEquals(1, rect.top);
    // In webkit the right margin is the calculated distance from right edge and
    // not the computed right margin so it is not reliable.
    // See https://bugs.webkit.org/show_bug.cgi?id=19828
    if (!userAgent.WEBKIT) {
      assertEquals(2, rect.right);
    }
    assertEquals(3, rect.bottom);
    assertEquals(4, rect.left);

    el.style.margin = '1mm 2em 3ex 4%';
    rect = getMarginBox(el);
    assertFalse(isNaN(rect.top));
    assertFalse(isNaN(rect.right));
    assertFalse(isNaN(rect.bottom));
    assertFalse(isNaN(rect.left));
    assertTrue(rect.top >= 0);
    // In webkit the right margin is the calculated distance from right edge and
    // not the computed right margin so it is not reliable.
    // See https://bugs.webkit.org/show_bug.cgi?id=19828
    if (!userAgent.WEBKIT) {
      assertTrue(rect.right >= 0);
    }
    assertTrue(rect.bottom >= 0);
    assertTrue(rect.left >= 0);
  },

  testGetBorderBox() {
    // Strict mode
    const el = $('size-e');
    const getBorderBox = googStyle.getBorderBox;

    // Clean up
    // style element has 100x100, no border and no padding
    el.style.padding = '';
    el.style.margin = '';
    el.style.borderWidth = '';
    el.style.width = '';
    el.style.height = '';

    el.style.borderWidth = '10px';
    let rect = getBorderBox(el);
    assertEquals(10, rect.left);
    assertEquals(10, rect.right);
    assertEquals(10, rect.top);
    assertEquals(10, rect.bottom);

    el.style.borderWidth = '0';
    rect = getBorderBox(el);
    assertEquals(0, rect.left);
    assertEquals(0, rect.right);
    assertEquals(0, rect.top);
    assertEquals(0, rect.bottom);

    el.style.borderWidth = '1px 2px 3px 4px';
    rect = getBorderBox(el);
    assertEquals(1, rect.top);
    assertEquals(2, rect.right);
    assertEquals(3, rect.bottom);
    assertEquals(4, rect.left);

    // % does not work for border widths in IE
    el.style.borderWidth = '1mm 2em 3ex 4pt';
    rect = getBorderBox(el);
    assertFalse(isNaN(rect.top));
    assertFalse(isNaN(rect.right));
    assertFalse(isNaN(rect.bottom));
    assertFalse(isNaN(rect.left));
    assertTrue(rect.top >= 0);
    assertTrue(rect.right >= 0);
    assertTrue(rect.bottom >= 0);
    assertTrue(rect.left >= 0);

    el.style.borderWidth = 'thin medium thick 1px';
    rect = getBorderBox(el);
    assertFalse(isNaN(rect.top));
    assertFalse(isNaN(rect.right));
    assertFalse(isNaN(rect.bottom));
    assertFalse(isNaN(rect.left));
    assertTrue(rect.top >= 0);
    assertTrue(rect.right >= 0);
    assertTrue(rect.bottom >= 0);
    assertTrue(rect.left >= 0);
  },

  testGetFontFamily() {
    // I tried to use common fonts for these tests. It's possible the test fails
    // because the testing platform doesn't have one of these fonts installed:
    //   Comic Sans MS or Century Schoolbook L
    //   Times
    //   Helvetica

    let tmpFont = googStyle.getFontFamily($('font-tag'));
    assertTrue(
        'FontFamily should be detectable when set via <font face>',
        'Times' == tmpFont || 'Times New Roman' == tmpFont);
    tmpFont = googStyle.getFontFamily($('small-text'));
    assertTrue(
        'Multiword fonts should be reported with quotes stripped.',
        'Comic Sans MS' == tmpFont || 'Century Schoolbook L' == tmpFont);
    // Firefox fails this test & retuns a generic 'monospace' instead of the
    // actually displayed font (e.g., "Times New").
    // tmpFont = goog.style.getFontFamily($('pre-font'));
    // assertEquals('<pre> tags should use a fixed-width font',
    //             'Times New',
    //             tmpFont);
    tmpFont = googStyle.getFontFamily($('inherit-font'));
    assertEquals(
        'Explicitly inherited fonts should be detectable', 'Helvetica',
        tmpFont);
    tmpFont = googStyle.getFontFamily($('times-font-family'));
    assertEquals(
        'Font-family set via style attribute should be detected', 'Times',
        tmpFont);
    tmpFont = googStyle.getFontFamily($('bold-font'));
    assertEquals(
        'Implicitly inherited font should be detected', 'Helvetica', tmpFont);
    tmpFont = googStyle.getFontFamily($('css-html-tag-redefinition'));
    assertEquals('HTML tag CSS rewrites should be detected', 'Times', tmpFont);
    tmpFont = googStyle.getFontFamily($('no-text-font-styles'));
    assertEquals(
        'Font family should exist even with no text', 'Helvetica', tmpFont);
    tmpFont = googStyle.getFontFamily($('icon-font'));
    assertNotEquals(
        'icon is a special font-family value', 'icon', tmpFont.toLowerCase());
    tmpFont = googStyle.getFontFamily($('font-style-badfont'));
    // Firefox fails this test and reports the specified "badFont", which is
    // obviously not displayed.
    // assertEquals('Invalid fonts should not be returned',
    //             'Helvetica',
    //             tmpFont);
    tmpFont = googStyle.getFontFamily($('img-font-test'));
    assertTrue(
        'Even img tags should inherit the document body\'s font',
        tmpFont != '');
    tmpFont = googStyle.getFontFamily($('nested-font'));
    assertEquals(
        'An element with nested content should be unaffected.', 'Arial',
        tmpFont);
    // IE raises an 'Invalid Argument' error when using the moveToElementText
    // method from the TextRange object with an element that is not attached to
    // a document.
    const element = googDom.createDom(
        TagName.SPAN, {style: 'font-family:Times,sans-serif;'}, 'some text');
    tmpFont = googStyle.getFontFamily(element);
    assertEquals(
        'Font should be correctly retrieved for element not attached' +
            ' to a document',
        'Times', tmpFont);
  },

  testGetFontSize() {
    assertEquals(
        'Font size should be determined even without any text', 30,
        googStyle.getFontSize($('no-text-font-styles')));
    assertEquals(
        'A 5em font should be 5x larger than its parent.', 150,
        googStyle.getFontSize($('css-html-tag-redefinition')));
    assertTrue(
        'Setting font size=-1 should result in a positive font size.',
        googStyle.getFontSize($('font-tag')) > 0);
    assertEquals(
        'Inheriting a 50% font-size should have no additional effect',
        googStyle.getFontSize($('font-style-badfont')),
        googStyle.getFontSize($('inherit-50pct-font')));
    assertTrue(
        'In pretty much any display, 3in should be > 8px',
        googStyle.getFontSize($('times-font-family')) >
            googStyle.getFontSize($('no-text-font-styles')));
    assertTrue(
        'With no applied styles, font-size should still be defined.',
        googStyle.getFontSize($('no-font-style')) > 0);
    assertEquals(
        '50% of 30px is 15', 15,
        googStyle.getFontSize($('font-style-badfont')));
    assertTrue(
        'x-small text should be smaller than small text',
        googStyle.getFontSize($('x-small-text')) <
            googStyle.getFontSize($('small-text')));
    // IE fails this test, the decimal portion of px lengths isn't reported
    // by getCascadedStyle. Firefox passes, but only because it ignores the
    // decimals altogether.
    // assertEquals('12.5px should be the same as 0.5em nested in a 25px node.',
    //             goog.style.getFontSize($('font-size-12-point-5-px')),
    //             goog.style.getFontSize($('font-size-50-pct-of-25-px')));

    assertEquals(
        'Font size should not doubly count em values', 2,
        googStyle.getFontSize($('em-font-size')));
  },

  testGetLengthUnits() {
    assertEquals('px', googStyle.getLengthUnits('15px'));
    assertEquals('%', googStyle.getLengthUnits('99%'));
    assertNull(googStyle.getLengthUnits(''));
  },

  testParseStyleAttribute() {
    const css = 'left: 0px; text-align: center';
    const expected = {'left': '0px', 'textAlign': 'center'};

    assertObjectEquals(expected, googStyle.parseStyleAttribute(css));
  },

  testToStyleAttribute() {
    const object = {'left': '0px', 'textAlign': 'center'};
    const expected = 'left:0px;text-align:center;';

    assertEquals(expected, googStyle.toStyleAttribute(object));
  },

  testStyleAttributePassthrough() {
    const object = {'left': '0px', 'textAlign': 'center'};

    assertObjectEquals(
        object,
        googStyle.parseStyleAttribute(googStyle.toStyleAttribute(object)));
  },

  testGetFloat() {
    assertEquals('', googStyle.getFloat($('no-float')));
    assertEquals('none', googStyle.getFloat($('float-none')));
    assertEquals('left', googStyle.getFloat($('float-left')));
  },

  testSetFloat() {
    const el = $('float-test');

    googStyle.setFloat(el, 'left');
    assertEquals('left', googStyle.getFloat(el));

    googStyle.setFloat(el, 'right');
    assertEquals('right', googStyle.getFloat(el));

    googStyle.setFloat(el, 'none');
    assertEquals('none', googStyle.getFloat(el));

    googStyle.setFloat(el, '');
    assertEquals('', googStyle.getFloat(el));
  },

  testIsElementShown() {
    const el = $('testEl');

    googStyle.setElementShown(el, false);
    assertFalse(googStyle.isElementShown(el));

    googStyle.setElementShown(el, true);
    assertTrue(googStyle.isElementShown(el));
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testGetOpacity() {
    const el1 = {style: {opacity: '0.3'}};

    const el2 = {style: {MozOpacity: '0.1'}};

    const el3 = {
      style: {filter: 'some:other,filter;alpha(opacity=25.5);alpha(more=100);'},
    };

    assertEquals(0.3, googStyle.getOpacity(el1));
    assertEquals(0.1, googStyle.getOpacity(el2));
    assertEquals(0.255, googStyle.getOpacity(el3));

    el1.style.opacity = '0';
    el2.style.MozOpacity = '0';
    el3.style.filter = 'some:other,filter;alpha(opacity=0);alpha(more=100);';

    assertEquals(0, googStyle.getOpacity(el1));
    assertEquals(0, googStyle.getOpacity(el2));
    assertEquals(0, googStyle.getOpacity(el3));

    el1.style.opacity = '';
    el2.style.MozOpacity = '';
    el3.style.filter = '';

    assertEquals('', googStyle.getOpacity(el1));
    assertEquals('', googStyle.getOpacity(el2));
    assertEquals('', googStyle.getOpacity(el3));

    const el4 = {style: {}};

    assertEquals('', googStyle.getOpacity(el4));
    assertEquals('', googStyle.getOpacity($('test-opacity')));
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testSetOpacity() {
    const el1 = {style: {opacity: '0.3'}};
    googStyle.setOpacity(el1, 0.8);

    const el2 = {style: {MozOpacity: '0.1'}};
    googStyle.setOpacity(el2, 0.5);

    const el3 = {style: {filter: 'alpha(opacity=25)'}};
    googStyle.setOpacity(el3, 0.1);

    assertEquals(0.8, Number(el1.style.opacity));
    assertEquals(0.5, Number(el2.style.MozOpacity));
    assertEquals('alpha(opacity=10)', el3.style.filter);

    googStyle.setOpacity(el1, 0);
    googStyle.setOpacity(el2, 0);
    googStyle.setOpacity(el3, 0);

    assertEquals(0, Number(el1.style.opacity));
    assertEquals(0, Number(el2.style.MozOpacity));
    assertEquals('alpha(opacity=0)', el3.style.filter);

    googStyle.setOpacity(el1, '');
    googStyle.setOpacity(el2, '');
    googStyle.setOpacity(el3, '');

    assertEquals('', el1.style.opacity);
    assertEquals('', el2.style.MozOpacity);
    assertEquals('', el3.style.filter);
  },

  testFramedPageOffset() {
    // Set up a complicated iframe ancestor chain.
    const iframe = createTestFrame();
    iframe.id = 'test-frame-offset';
    try {
      document.body.appendChild(iframe);
      const iframeDoc = googDom.getFrameContentDocument(iframe);
      const iframeWindow = googDom.getWindow(iframeDoc);

      const iframePos = 'style="display:block;position:absolute;' +
          'top:50px;left:50px;width:50px;height:50px;"';
      iframeDoc.write(
          `<iframe id="test-frame-offset-2" ${iframePos}></iframe>` +
          '<div id="test-element-2" ' +
          ' style="position:absolute;left:300px;top:300px">hi mom!</div>');
      iframeDoc.close();
      const iframe2 = iframeDoc.getElementById('test-frame-offset-2');
      const testElement2 = iframeDoc.getElementById('test-element-2');
      const iframeDoc2 = googDom.getFrameContentDocument(iframe2);
      const iframeWindow2 = googDom.getWindow(iframeDoc2);

      iframeDoc2.write(
          '<div id="test-element-3" ' +
          ' style="position:absolute;left:500px;top:500px">hi mom!</div>');
      iframeDoc2.close();
      const testElement3 = iframeDoc2.getElementById('test-element-3');

      assertCoordinateApprox(
          300, 300, 0, googStyle.getPageOffset(testElement2));
      assertCoordinateApprox(
          500, 500, 0, googStyle.getPageOffset(testElement3));

      assertCoordinateApprox(
          350, 350, 0, googStyle.getFramedPageOffset(testElement2, window));
      assertCoordinateApprox(
          300, 300, 0,
          googStyle.getFramedPageOffset(testElement2, iframeWindow));

      assertCoordinateApprox(
          600, 600, 0, googStyle.getFramedPageOffset(testElement3, window));
      assertCoordinateApprox(
          550, 550, 0,
          googStyle.getFramedPageOffset(testElement3, iframeWindow));
      assertCoordinateApprox(
          500, 500, 0,
          googStyle.getFramedPageOffset(testElement3, iframeWindow2));

      // Scroll the iframes a bit.
      window.scrollBy(0, 5);
      iframeWindow.scrollBy(0, 11);
      iframeWindow2.scrollBy(0, 18);

      // On Firefox 2, scrolling inner iframes causes off by one errors
      // in the page position, because we're using screen coords to compute
      // them.
      assertCoordinateApprox(
          300, 300, 2, googStyle.getPageOffset(testElement2));
      assertCoordinateApprox(
          500, 500, 2, googStyle.getPageOffset(testElement3));

      assertCoordinateApprox(
          350, 350 - 11, 2,
          googStyle.getFramedPageOffset(testElement2, window));
      assertCoordinateApprox(
          300, 300, 2,
          googStyle.getFramedPageOffset(testElement2, iframeWindow));

      assertCoordinateApprox(
          600, 600 - 18 - 11, 2,
          googStyle.getFramedPageOffset(testElement3, window));
      assertCoordinateApprox(
          550, 550 - 18, 2,
          googStyle.getFramedPageOffset(testElement3, iframeWindow));
      assertCoordinateApprox(
          500, 500, 2,
          googStyle.getFramedPageOffset(testElement3, iframeWindow2));

      // In IE, if the element is in a frame that's been removed from the DOM
      // and relativeWin is not that frame's contentWindow, the contentWindow's
      // parent reference points to itself. We want to guarantee that we don't
      // fall into an infinite loop.
      const iframeParent = iframe.parentElement;
      iframeParent.removeChild(iframe);
      // We don't check the value returned as it differs by browser. 0,0 for
      // Chrome and FF. IE returns 30000 or 30198 for x in IE8-9 and 300 in
      // IE10-11
      googStyle.getFramedPageOffset(testElement2, window);
    } finally {
      googDom.removeNode(iframe);
    }
  },

  testTranslateRectForAnotherFrame() {
    let rect = new GoogRect(1, 2, 3, 4);
    const thisDom = googDom.getDomHelper();
    googStyle.translateRectForAnotherFrame(rect, thisDom, thisDom);
    assertEquals(1, rect.left);
    assertEquals(2, rect.top);
    assertEquals(3, rect.width);
    assertEquals(4, rect.height);

    let iframe = $('test-translate-frame-standard');
    let iframeDoc = googDom.getFrameContentDocument(iframe);
    let iframeDom = googDom.getDomHelper(iframeDoc);
    // Cannot rely on iframe starting at origin.
    iframeDom.getWindow().scrollTo(0, 0);
    // iframe is at (100, 150) and its body is not scrolled.
    rect = new GoogRect(1, 2, 3, 4);
    googStyle.translateRectForAnotherFrame(rect, iframeDom, thisDom);
    assertEquals(1 + 100, rect.left);
    assertRoughlyEquals(2 + 150, rect.top, .1);
    assertEquals(3, rect.width);
    assertEquals(4, rect.height);

    iframeDom.getWindow().scrollTo(11, 13);
    rect = new GoogRect(1, 2, 3, 4);
    googStyle.translateRectForAnotherFrame(rect, iframeDom, thisDom);
    assertEquals(1 + 100 - 11, rect.left);
    assertRoughlyEquals(2 + 150 - 13, rect.top, .1);
    assertEquals(3, rect.width);
    assertEquals(4, rect.height);

    iframe = $('test-translate-frame-quirk');
    iframeDoc = googDom.getFrameContentDocument(iframe);
    iframeDom = googDom.getDomHelper(iframeDoc);
    // Cannot rely on iframe starting at origin.
    iframeDom.getWindow().scrollTo(0, 0);
    // iframe is at (100, 350) and its body is not scrolled.
    rect = new GoogRect(1, 2, 3, 4);
    googStyle.translateRectForAnotherFrame(rect, iframeDom, thisDom);
    assertEquals(1 + 100, rect.left);
    assertRoughlyEquals(2 + 350, rect.top, .1);
    assertEquals(3, rect.width);
    assertEquals(4, rect.height);

    iframeDom.getWindow().scrollTo(11, 13);
    rect = new GoogRect(1, 2, 3, 4);
    googStyle.translateRectForAnotherFrame(rect, iframeDom, thisDom);
    assertEquals(1 + 100 - 11, rect.left);
    assertRoughlyEquals(2 + 350 - 13, rect.top, .1);
    assertEquals(3, rect.width);
    assertEquals(4, rect.height);
  },

  testGetVisibleRectForElement() {
    const container = googDom.getElement('test-visible');
    let el = googDom.getElement('test-visible-el');
    const dom = googDom.getDomHelper(el);
    const winScroll = dom.getDocumentScroll();
    const winSize = dom.getViewportSize();

    // Skip this test if the window size is small.  Firefox3/Linux in Selenium
    // sometimes fails without this check.
    if (winSize.width < 20 || winSize.height < 20) {
      return;
    }

    // Move the container element to the window's viewport.
    const h = winSize.height < 100 ? winSize.height / 2 : 100;
    googStyle.setSize(container, winSize.width / 2, h);
    googStyle.setPosition(container, 8, winScroll.y + winSize.height - h);
    let visible = googStyle.getVisibleRectForElement(el);
    let bounds = googStyle.getBounds(container);
    // VisibleRect == Bounds rect of the offsetParent
    assertNotNull(visible);
    assertEquals(bounds.left, visible.left);
    assertEquals(bounds.top, visible.top);
    assertEquals(bounds.left + bounds.width, visible.right);
    assertEquals(bounds.top + bounds.height, visible.bottom);

    // Move a part of the container element to outside of the viewpoert.
    googStyle.setPosition(container, 8, winScroll.y + winSize.height - h / 2);
    visible = googStyle.getVisibleRectForElement(el);
    bounds = googStyle.getBounds(container);
    // Confirm VisibleRect == Intersection of the bounds rect of the
    // offsetParent and the viewport.
    assertNotNull(visible);
    assertEquals(bounds.left, visible.left);
    assertEquals(bounds.top, visible.top);
    assertEquals(bounds.left + bounds.width, visible.right);
    assertEquals(winScroll.y + winSize.height, visible.bottom);

    // Move the container element to outside of the viewpoert.
    googStyle.setPosition(container, 8, winScroll.y + winSize.height * 2);
    visible = googStyle.getVisibleRectForElement(el);
    assertNull(visible);

    // Test the case with body element of height 0
    const iframe = googDom.getElement('test-visible-frame');
    const iframeDoc = googDom.getFrameContentDocument(iframe);
    el = iframeDoc.getElementById('test-visible');
    visible = googStyle.getVisibleRectForElement(el);

    const iframeViewportSize = googDom.getDomHelper(el).getViewportSize();
    // NOTE(chrishenry): For iframe, the clipping viewport is always the iframe
    // viewport, and not the actual browser viewport.
    assertNotNull(visible);
    assertEquals(0, visible.top);
    assertEquals(iframeViewportSize.height, visible.bottom);
    assertEquals(0, visible.left);
    assertEquals(iframeViewportSize.width, visible.right);
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testGetVisibleRectForElementWithBodyScrolled() {
    const container = googDom.getElement('test-visible2');
    const dom = googDom.getDomHelper(container);
    const el = dom.createDom(TagName.DIV, undefined, 'Test');
    el.style.position = 'absolute';
    dom.append(container, el);

    container.style.position = 'absolute';
    googStyle.setPosition(container, 20, 500);
    googStyle.setSize(container, 100, 150);

    // Scroll body container such that container is exactly at top.
    window.scrollTo(0, 500);
    let visibleRect = googStyle.getVisibleRectForElement(el);
    assertNotNull(visibleRect);
    assertRoughlyEquals(500, visibleRect.top, EPSILON);
    assertRoughlyEquals(20, visibleRect.left, EPSILON);
    assertRoughlyEquals(650, visibleRect.bottom, EPSILON);
    assertRoughlyEquals(120, visibleRect.right, EPSILON);

    // Top 100px is clipped by window viewport.
    window.scrollTo(0, 600);
    visibleRect = googStyle.getVisibleRectForElement(el);
    assertNotNull(visibleRect);
    assertRoughlyEquals(600, visibleRect.top, EPSILON);
    assertRoughlyEquals(20, visibleRect.left, EPSILON);
    assertRoughlyEquals(650, visibleRect.bottom, EPSILON);
    assertRoughlyEquals(120, visibleRect.right, EPSILON);

    const winSize = dom.getViewportSize();

    // Left 50px is clipped by window viewport.
    // Right part is clipped by window viewport.
    googStyle.setSize(container, 10000, 150);
    window.scrollTo(70, 500);
    visibleRect = googStyle.getVisibleRectForElement(el);
    assertNotNull(visibleRect);
    assertRoughlyEquals(500, visibleRect.top, EPSILON);
    assertRoughlyEquals(70, visibleRect.left, EPSILON);
    assertRoughlyEquals(650, visibleRect.bottom, EPSILON);
    assertRoughlyEquals(70 + winSize.width, visibleRect.right, EPSILON);

    // Bottom part is clipped by window viewport.
    googStyle.setSize(container, 100, 2000);
    window.scrollTo(0, 500);
    visibleRect = googStyle.getVisibleRectForElement(el);
    assertNotNull(visibleRect);
    assertRoughlyEquals(500, visibleRect.top, EPSILON);
    assertRoughlyEquals(20, visibleRect.left, EPSILON);
    assertRoughlyEquals(120, visibleRect.right, EPSILON);
    assertRoughlyEquals(500 + winSize.height, visibleRect.bottom, EPSILON);

    googStyle.setPosition(container, 10000, 10000);
    assertNull(googStyle.getVisibleRectForElement(el));
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testGetVisibleRectForElementWithNestedAreaAndNonOffsetAncestor() {
    // IE7 quirks mode somehow consider container2 below as offset parent
    // of the element, which is incorrect.
    if (userAgent.IE && !userAgent.isDocumentModeOrHigher(8) &&
        !googDom.isCss1CompatMode()) {
      return;
    }

    const container = googDom.getElement('test-visible2');
    const dom = googDom.getDomHelper(container);
    const container2 = dom.createDom(TagName.DIV);
    const el = dom.createDom(TagName.DIV, undefined, 'Test');
    el.style.position = 'absolute';
    dom.append(container, container2);
    dom.append(container2, el);

    container.style.position = 'absolute';
    googStyle.setPosition(container, 20, 500);
    googStyle.setSize(container, 100, 150);

    // container2 is a scrollable container but is not an offsetParent of
    // the element. It is ignored in the computation.
    container2.style.overflow = 'hidden';
    container2.style.marginTop = '50px';
    container2.style.marginLeft = '100px';
    googStyle.setSize(container2, 150, 100);

    // Scroll body container such that container is exactly at top.
    window.scrollTo(0, 500);
    let visibleRect = googStyle.getVisibleRectForElement(el);
    assertNotNull(visibleRect);
    assertRoughlyEquals(500, visibleRect.top, EPSILON);
    assertRoughlyEquals(20, visibleRect.left, EPSILON);
    assertRoughlyEquals(650, visibleRect.bottom, EPSILON);
    assertRoughlyEquals(120, visibleRect.right, EPSILON);

    // Top 100px is clipped by window viewport.
    window.scrollTo(0, 600);
    visibleRect = googStyle.getVisibleRectForElement(el);
    assertNotNull(visibleRect);
    assertRoughlyEquals(600, visibleRect.top, EPSILON);
    assertRoughlyEquals(20, visibleRect.left, EPSILON);
    assertRoughlyEquals(650, visibleRect.bottom, EPSILON);
    assertRoughlyEquals(120, visibleRect.right, EPSILON);

    const winSize = dom.getViewportSize();

    // Left 50px is clipped by window viewport.
    // Right part is clipped by window viewport.
    googStyle.setSize(container, 10000, 150);
    window.scrollTo(70, 500);
    visibleRect = googStyle.getVisibleRectForElement(el);
    assertNotNull(visibleRect);
    assertRoughlyEquals(500, visibleRect.top, EPSILON);
    assertRoughlyEquals(70, visibleRect.left, EPSILON);
    assertRoughlyEquals(650, visibleRect.bottom, EPSILON);
    assertRoughlyEquals(70 + winSize.width, visibleRect.right, EPSILON);

    // Bottom part is clipped by window viewport.
    googStyle.setSize(container, 100, 2000);
    window.scrollTo(0, 500);
    visibleRect = googStyle.getVisibleRectForElement(el);
    assertNotNull(visibleRect);
    assertRoughlyEquals(500, visibleRect.top, EPSILON);
    assertRoughlyEquals(20, visibleRect.left, EPSILON);
    assertRoughlyEquals(120, visibleRect.right, EPSILON);
    assertRoughlyEquals(500 + winSize.height, visibleRect.bottom, EPSILON);

    googStyle.setPosition(container, 10000, 10000);
    assertNull(googStyle.getVisibleRectForElement(el));
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testGetVisibleRectForElementInsideNestedScrollableArea() {
    const container = googDom.getElement('test-visible2');
    const dom = googDom.getDomHelper(container);
    const container2 = dom.createDom(TagName.DIV);
    const el = dom.createDom(TagName.DIV, undefined, 'Test');
    el.style.position = 'absolute';
    dom.append(container, container2);
    dom.append(container2, el);

    container.style.position = 'absolute';
    googStyle.setPosition(container, 100 /* left */, 500 /* top */);
    googStyle.setSize(container, 300 /* width */, 300 /* height */);

    container2.style.overflow = 'hidden';
    container2.style.position = 'relative';
    googStyle.setPosition(container2, 100, 50);
    googStyle.setSize(container2, 150, 100);

    // Scroll body container such that container is exactly at top.
    window.scrollTo(0, 500);
    let visibleRect = googStyle.getVisibleRectForElement(el);
    assertNotNull(visibleRect);
    assertRoughlyEquals(550, visibleRect.top, EPSILON);
    assertRoughlyEquals(200, visibleRect.left, EPSILON);
    assertRoughlyEquals(650, visibleRect.bottom, EPSILON);
    assertRoughlyEquals(350, visibleRect.right, EPSILON);

    // Left 50px is clipped by container.
    googStyle.setPosition(container2, -50, 50);
    visibleRect = googStyle.getVisibleRectForElement(el);
    assertNotNull(visibleRect);
    assertRoughlyEquals(550, visibleRect.top, EPSILON);
    assertRoughlyEquals(100, visibleRect.left, EPSILON);
    assertRoughlyEquals(650, visibleRect.bottom, EPSILON);
    assertRoughlyEquals(200, visibleRect.right, EPSILON);

    // Right part is clipped by container.
    googStyle.setPosition(container2, 100, 50);
    googStyle.setWidth(container2, 1000, 100);
    visibleRect = googStyle.getVisibleRectForElement(el);
    assertNotNull(visibleRect);
    assertRoughlyEquals(550, visibleRect.top, EPSILON);
    assertRoughlyEquals(200, visibleRect.left, EPSILON);
    assertRoughlyEquals(650, visibleRect.bottom, EPSILON);
    assertRoughlyEquals(400, visibleRect.right, EPSILON);

    // Top 50px is clipped by container.
    googStyle.setStyle(container2, 'width', '150px');
    googStyle.setStyle(container2, 'top', '-50px');
    visibleRect = googStyle.getVisibleRectForElement(el);
    assertNotNull(visibleRect);
    assertRoughlyEquals(500, visibleRect.top, EPSILON);
    assertRoughlyEquals(200, visibleRect.left, EPSILON);
    assertRoughlyEquals(550, visibleRect.bottom, EPSILON);
    assertRoughlyEquals(350, visibleRect.right, EPSILON);

    // Bottom part is clipped by container.
    googStyle.setStyle(container2, 'top', '50px');
    googStyle.setStyle(container2, 'height', '1000px');
    visibleRect = googStyle.getVisibleRectForElement(el);
    assertNotNull(visibleRect);
    assertRoughlyEquals(550, visibleRect.top, EPSILON);
    assertRoughlyEquals(200, visibleRect.left, EPSILON);
    assertRoughlyEquals(800, visibleRect.bottom, EPSILON);
    assertRoughlyEquals(350, visibleRect.right, EPSILON);

    // Outside viewport.
    googStyle.setStyle(container2, 'top', '10000px');
    googStyle.setStyle(container2, 'left', '10000px');
    assertNull(googStyle.getVisibleRectForElement(el));
  },

  testScrollIntoContainerViewQuirks() {
    if (googDom.isCss1CompatMode()) return;

    const container = googDom.getElement('scrollable-container');

    // Scroll the minimum amount to make the elements visible.
    googStyle.scrollIntoContainerView(googDom.getElement('item7'), container);
    assertEquals('scroll to item7', 79, container.scrollTop);
    googStyle.scrollIntoContainerView(googDom.getElement('item8'), container);
    assertEquals('scroll to item8', 100, container.scrollTop);
    googStyle.scrollIntoContainerView(googDom.getElement('item7'), container);
    assertEquals('item7 still visible', 100, container.scrollTop);
    googStyle.scrollIntoContainerView(googDom.getElement('item1'), container);
    assertEquals('scroll to item1', 17, container.scrollTop);

    // Center the element in the first argument.
    googStyle.scrollIntoContainerView(
        googDom.getElement('item1'), container, true);
    assertEquals('center item1', 0, container.scrollTop);
    googStyle.scrollIntoContainerView(
        googDom.getElement('item4'), container, true);
    assertEquals('center item4', 48, container.scrollTop);

    // The element is higher than the container.
    googDom.getElement('item3').style.height = '140px';
    googStyle.scrollIntoContainerView(googDom.getElement('item3'), container);
    assertEquals('show item3 with increased height', 59, container.scrollTop);
    googStyle.scrollIntoContainerView(
        googDom.getElement('item3'), container, true);
    assertEquals('center item3 with increased height', 87, container.scrollTop);
    googDom.getElement('item3').style.height = '';

    // Scroll to non-integer position.
    googDom.getElement('item4').style.height = '21px';
    googStyle.scrollIntoContainerView(
        googDom.getElement('item4'), container, true);
    assertEquals('scroll position is rounded down', 48, container.scrollTop);
    googDom.getElement('item4').style.height = '';
  },

  testScrollIntoContainerViewStandard() {
    if (!googDom.isCss1CompatMode()) return;

    const container = googDom.getElement('scrollable-container');

    // Scroll the minimum amount to make the elements visible.
    googStyle.scrollIntoContainerView(googDom.getElement('item7'), container);
    assertEquals('scroll to item7', 115, container.scrollTop);
    googStyle.scrollIntoContainerView(googDom.getElement('item8'), container);
    assertEquals('scroll to item8', 148, container.scrollTop);
    googStyle.scrollIntoContainerView(googDom.getElement('item7'), container);
    assertEquals('item7 still visible', 148, container.scrollTop);
    googStyle.scrollIntoContainerView(googDom.getElement('item1'), container);
    assertEquals('scroll to item1', 17, container.scrollTop);

    // Center the element in the first argument.
    googStyle.scrollIntoContainerView(
        googDom.getElement('item1'), container, true);
    assertEquals('center item1', 0, container.scrollTop);
    googStyle.scrollIntoContainerView(
        googDom.getElement('item4'), container, true);
    assertEquals('center item4', 66, container.scrollTop);

    // The element is higher than the container.
    googDom.getElement('item3').style.height = '140px';
    googStyle.scrollIntoContainerView(googDom.getElement('item3'), container);
    assertEquals('show item3 with increased height', 83, container.scrollTop);
    googStyle.scrollIntoContainerView(
        googDom.getElement('item3'), container, true);
    assertEquals('center item3 with increased height', 93, container.scrollTop);
    googDom.getElement('item3').style.height = '';

    // Scroll to non-integer position.
    googDom.getElement('item4').style.height = '21px';
    googStyle.scrollIntoContainerView(
        googDom.getElement('item4'), container, true);
    assertEquals('scroll position is rounded down', 66, container.scrollTop);
    googDom.getElement('item4').style.height = '';
  },

  testScrollIntoContainerViewSvg() {
    if (!googDom.isCss1CompatMode()) {
      return;
    }

    const svgEl = document.createElementNS &&
        document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    if (!svgEl || svgEl.getAttribute('transform') == '' ||
        (userAgent.WEBKIT && !userAgent.isVersionOrHigher(534.8))) {
      // SVG not supported, or getBoundingClientRect not supported on SVG
      // elements.
      return;
    }

    const assertEqualsForSvgPos = (expected, actual) => {
      if (userAgent.EDGE_OR_IE) {
        // The bounding size is 1 larger than the SVG element in IE. The
        // scrollTop value maybe 1 less or 1 more than the expected value
        // depending on the scroll direction.
        assertRoughlyEquals(expected, actual, 1);
      } else {
        assertEquals(expected, actual);
      }
    };

    const svgItem1 = googDom.getElement('svg-item1');
    const svgItem2 = googDom.getElement('svg-item2');
    const svgItem3 = googDom.getElement('svg-item3');

    // Scroll the minimum amount to make the elements visible.
    const container = googDom.getElement('svg-container');
    googStyle.scrollIntoContainerView(svgItem1, container);
    assertEquals(0, container.scrollTop);
    googStyle.scrollIntoContainerView(svgItem2, container);
    assertEqualsForSvgPos(50, container.scrollTop);
    googStyle.scrollIntoContainerView(svgItem3, container);
    assertEqualsForSvgPos(150, container.scrollTop);
    googStyle.scrollIntoContainerView(svgItem2, container);
    assertEqualsForSvgPos(100, container.scrollTop);

    // Center the element in the first argument.
    googStyle.scrollIntoContainerView(svgItem2, container, true);
    assertEqualsForSvgPos(75, container.scrollTop);
    googStyle.scrollIntoContainerView(svgItem3, container, true);
    assertEqualsForSvgPos(175, container.scrollTop);

    // The element is higher than the container.
    svgItem3.setAttribute('height', 200);
    googStyle.scrollIntoContainerView(svgItem3, container);
    assertEqualsForSvgPos(200, container.scrollTop);
    googStyle.scrollIntoContainerView(svgItem3, container, true);
    assertEqualsForSvgPos(225, container.scrollTop);

    // Scroll to non-integer position.
    svgItem3.setAttribute('height', 75);
    googStyle.scrollIntoContainerView(svgItem3, container, true);
    // Scroll position is rounded down from 162.5
    assertEqualsForSvgPos(162, container.scrollTop);
    svgItem3.setAttribute('height', 100);
  },

  testOffsetParent() {
    const parent = googDom.getElement('offset-parent');
    const child = googDom.getElement('offset-child');
    assertEquals(parent, googStyle.getOffsetParent(child));
  },

  testOverflowOffsetParent() {
    const parent = googDom.getElement('offset-parent-overflow');
    const child = googDom.getElement('offset-child-overflow');
    assertEquals(parent, googStyle.getOffsetParent(child));
  },

  /** @suppress {missingProperties} suppression added to enable type checking */
  testShadowDomOffsetParent() {
    // Ignore browsers that don't support shadowDOM.
    if (!document.createShadowRoot) {
      return;
    }

    const parent = googDom.createDom(TagName.DIV);
    parent.style.position = 'relative';
    const host = googDom.createDom(TagName.DIV);
    googDom.appendChild(parent, host);
    const root = host.createShadowRoot();
    const child = googDom.createDom(TagName.DIV);
    googDom.appendChild(root, child);

    assertEquals(parent, googStyle.getOffsetParent(child));
  },

  testGetViewportPageOffset() {
    try {
      const testViewport = googDom.getElement('test-viewport');
      testViewport.style.height = '5000px';
      testViewport.style.width = '5000px';
      let offset = googStyle.getViewportPageOffset(document);
      assertEquals(0, offset.x);
      assertEquals(0, offset.y);

      window.scrollTo(0, 100);
      offset = googStyle.getViewportPageOffset(document);
      assertEquals(0, offset.x);
      assertEquals(100, offset.y);

      window.scrollTo(100, 0);
      offset = googStyle.getViewportPageOffset(document);
      assertEquals(100, offset.x);
      assertEquals(0, offset.y);
    } catch (e) {
      expectedFailures.handleException(e);
    }
  },

  testGetsTranslation() {
    const element = document.getElementById('translation');

    if (userAgent.IE) {
      if (!userAgent.isDocumentModeOrHigher(9) ||
          (!googDom.isCss1CompatMode() &&
           !userAgent.isDocumentModeOrHigher(10))) {
        // 'CSS transforms were introduced in IE9, but only in standards mode
        // later browsers support the translations in quirks mode.
        return;
      }
    }

    // First check the element is actually translated, and we haven't missed
    // one of the vendor-specific transform properties
    const position = googStyle.getClientPosition(element);
    /** @suppress {checkTypes} suppression added to enable type checking */
    const translation = googStyle.getCssTranslation(element);
    const expectedTranslation = new Coordinate(20, 30);

    assertEquals(30, position.x);
    assertRoughlyEquals(40, position.y, .1);
    assertObjectEquals(expectedTranslation, translation);
  },

  /**
   * Test for the proper vendor style name for a CSS property
   * with a vendor prefix for Webkit.
   * @suppress {visibility,checkTypes} suppression added to enable type checking
   */
  testGetVendorStyleNameWebkit() {
    const mockElement = {'style': {'WebkitTransformOrigin': ''}};

    assertUserAgent([UserAgents.WEBKIT], 'WebKit');
    assertEquals(
        '-webkit-transform-origin',
        googStyle.getVendorStyleName_(mockElement, 'transform-origin'));
  },

  /**
   * Test for the proper vendor style name for a CSS property
   * when it exists without a vendor prefix for Webkit.
   * @suppress {visibility,checkTypes} suppression added to enable type checking
   */
  testGetVendorStyleNameWebkitNoPrefix() {
    const mockElement = {
      'style': {'WebkitTransformOrigin': '', 'transformOrigin': ''},
    };

    assertUserAgent([UserAgents.WEBKIT], 'WebKit');
    assertEquals(
        'transform-origin',
        googStyle.getVendorStyleName_(mockElement, 'transform-origin'));
  },

  /**
   * Test for the proper vendor style name for a CSS property
   * with a vendor prefix for Gecko.
   * @suppress {visibility,checkTypes} suppression added to enable type checking
   */
  testGetVendorStyleNameGecko() {
    const mockElement = {'style': {'MozTransformOrigin': ''}};

    assertUserAgent([UserAgents.GECKO], 'Gecko', 'Gecko');
    assertEquals(
        '-moz-transform-origin',
        googStyle.getVendorStyleName_(mockElement, 'transform-origin'));
  },

  /**
   * Test for the proper vendor style name for a CSS property
   * when it exists without a vendor prefix for Gecko.
   * @suppress {visibility,checkTypes} suppression added to enable type checking
   */
  testGetVendorStyleNameGeckoNoPrefix() {
    const mockElement = {
      'style': {'MozTransformOrigin': '', 'transformOrigin': ''},
    };

    assertUserAgent([UserAgents.GECKO], 'Gecko', 'Gecko');
    assertEquals(
        'transform-origin',
        googStyle.getVendorStyleName_(mockElement, 'transform-origin'));
  },

  /**
   * Test for the proper vendor style name for a CSS property
   * with a vendor prefix for IE.
   * @suppress {visibility,checkTypes} suppression added to enable type checking
   */
  testGetVendorStyleNameIE() {
    const mockElement = {'style': {'msTransformOrigin': ''}};

    assertUserAgent([UserAgents.IE], 'MSIE');
    assertEquals(
        '-ms-transform-origin',
        googStyle.getVendorStyleName_(mockElement, 'transform-origin'));
  },

  /**
   * Test for the proper vendor style name for a CSS property
   * when it exists without a vendor prefix for IE.
   * @suppress {visibility,checkTypes} suppression added to enable type checking
   */
  testGetVendorStyleNameIENoPrefix() {
    const mockElement = {
      'style': {'msTransformOrigin': '', 'transformOrigin': ''},
    };

    assertUserAgent([UserAgents.IE], 'MSIE');
    assertEquals(
        'transform-origin',
        googStyle.getVendorStyleName_(mockElement, 'transform-origin'));
  },


  /**
   * Test for the proper vendor style name for a CSS property
   * with a vendor prefix for Webkit.
   * @suppress {visibility,checkTypes} suppression added to enable type checking
   */
  testGetVendorJsStyleNameWebkit() {
    const mockElement = {'style': {'WebkitTransformOrigin': ''}};

    assertUserAgent([UserAgents.WEBKIT], 'WebKit');
    assertEquals(
        'WebkitTransformOrigin',
        googStyle.getVendorJsStyleName_(mockElement, 'transform-origin'));
  },

  /**
   * Test for the proper vendor style name for a CSS property
   * when it exists without a vendor prefix for Webkit.
   * @suppress {visibility,checkTypes} suppression added to enable type checking
   */
  testGetVendorJsStyleNameWebkitNoPrefix() {
    const mockElement = {
      'style': {'WebkitTransformOrigin': '', 'transformOrigin': ''},
    };

    assertUserAgent([UserAgents.WEBKIT], 'WebKit');
    assertEquals(
        'transformOrigin',
        googStyle.getVendorJsStyleName_(mockElement, 'transform-origin'));
  },

  /**
   * Test for the proper vendor style name for a CSS property
   * with a vendor prefix for Gecko.
   * @suppress {visibility,checkTypes} suppression added to enable type checking
   */
  testGetVendorJsStyleNameGecko() {
    const mockElement = {'style': {'MozTransformOrigin': ''}};

    assertUserAgent([UserAgents.GECKO], 'Gecko', 'Gecko');
    assertEquals(
        'MozTransformOrigin',
        googStyle.getVendorJsStyleName_(mockElement, 'transform-origin'));
  },

  /**
   * Test for the proper vendor style name for a CSS property
   * when it exists without a vendor prefix for Gecko.
   * @suppress {visibility,checkTypes} suppression added to enable type checking
   */
  testGetVendorJsStyleNameGeckoNoPrefix() {
    const mockElement = {
      'style': {'MozTransformOrigin': '', 'transformOrigin': ''},
    };

    assertUserAgent([UserAgents.GECKO], 'Gecko', 'Gecko');
    assertEquals(
        'transformOrigin',
        googStyle.getVendorJsStyleName_(mockElement, 'transform-origin'));
  },

  /**
   * Test for the proper vendor style name for a CSS property
   * with a vendor prefix for IE.
   * @suppress {visibility,checkTypes} suppression added to enable type checking
   */
  testGetVendorJsStyleNameIE() {
    const mockElement = {'style': {'msTransformOrigin': ''}};

    assertUserAgent([UserAgents.IE], 'MSIE');
    assertEquals(
        'msTransformOrigin',
        googStyle.getVendorJsStyleName_(mockElement, 'transform-origin'));
  },

  /**
   * Test for the proper vendor style name for a CSS property
   * when it exists without a vendor prefix for IE.
   * @suppress {visibility,checkTypes} suppression added to enable type checking
   */
  testGetVendorJsStyleNameIENoPrefix() {
    const mockElement = {
      'style': {'msTransformOrigin': '', 'transformOrigin': ''},
    };

    assertUserAgent([UserAgents.IE], 'MSIE');
    assertEquals(
        'transformOrigin',
        googStyle.getVendorJsStyleName_(mockElement, 'transform-origin'));
  },



  /**
   * Test for the setting a style name for a CSS property
   * with a vendor prefix for Mozilla.
   * @suppress {checkTypes} suppression added to enable type checking
   */
  testSetVendorStyleGecko() {
    const mockElement = {'style': {'MozTransform': ''}};
    const styleValue = 'translate3d(0,0,0)';

    assertUserAgent([UserAgents.GECKO], 'Gecko', 'Gecko');
    googStyle.setStyle(mockElement, 'transform', styleValue);
    assertEquals(styleValue, mockElement.style.MozTransform);
  },

  /**
   * Test for the setting a style name for a CSS property
   * with a vendor prefix for IE.
   * @suppress {checkTypes} suppression added to enable type checking
   */
  testSetVendorStyleIE() {
    const mockElement = {'style': {'msTransform': ''}};
    const styleValue = 'translate3d(0,0,0)';

    assertUserAgent([UserAgents.IE], 'MSIE');
    googStyle.setStyle(mockElement, 'transform', styleValue);
    assertEquals(styleValue, mockElement.style.msTransform);
  },


  /**
   * Test for the getting a style name for a CSS property
   * with a vendor prefix for Webkit.
   * @suppress {checkTypes} suppression added to enable type checking
   */
  testGetVendorStyleWebkit() {
    const mockElement = {'style': {'WebkitTransform': ''}};
    const styleValue = 'translate3d(0,0,0)';

    assertUserAgent([UserAgents.WEBKIT], 'WebKit');
    googStyle.setStyle(mockElement, 'transform', styleValue);
    assertEquals(styleValue, googStyle.getStyle(mockElement, 'transform'));
  },

  /**
   * Test for the getting a style name for a CSS property
   * with a vendor prefix for Mozilla.
   * @suppress {checkTypes} suppression added to enable type checking
   */
  testGetVendorStyleGecko() {
    const mockElement = {'style': {'MozTransform': ''}};
    const styleValue = 'translate3d(0,0,0)';

    assertUserAgent([UserAgents.GECKO], 'Gecko', 'Gecko');
    googStyle.setStyle(mockElement, 'transform', styleValue);
    assertEquals(styleValue, googStyle.getStyle(mockElement, 'transform'));
  },

  /**
   * Test for the getting a style name for a CSS property
   * with a vendor prefix for IE.
   * @suppress {checkTypes} suppression added to enable type checking
   */
  testGetVendorStyleIE() {
    const mockElement = {'style': {'msTransform': ''}};
    const styleValue = 'translate3d(0,0,0)';

    assertUserAgent([UserAgents.IE], 'MSIE');
    googStyle.setStyle(mockElement, 'transform', styleValue);
    assertEquals(styleValue, googStyle.getStyle(mockElement, 'transform'));
  },


  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testParseStyleAttributeWithColon() {
    // Regression test for https://github.com/google/closure-library/issues/127.
    const cssObj = googStyle.parseStyleAttribute(
        'left: 0px; text-align: center; background-image: ' +
        'url(http://www.google.ca/Test.gif); -ms-filter: ' +
        'progid:DXImageTransform.Microsoft.MotionBlur(strength=50), ' +
        'progid:DXImageTransform.Microsoft.BasicImage(mirror=1);');
    assertEquals('url(http://www.google.ca/Test.gif)', cssObj.backgroundImage);
    assertEquals(
        'progid:DXImageTransform.Microsoft.MotionBlur(strength=50), ' +
            'progid:DXImageTransform.Microsoft.BasicImage(mirror=1)',
        cssObj.MsFilter);
  },
});

/**
 * @return {!HTMLIFrameElement}
 */
function createTestFrame() {
  const frame = googDom.createDom(TagName.IFRAME);
  safe.setIframeSrc(
      frame, TrustedResourceUrl.fromConstant(Const.from('about:blank')));
  return frame;
}
