text: var Plotly = require('@lib');
  var Plots = require('@src/plots/plots');
  var Lib = require('@src/lib');
  
  var ScatterMapbox = require('@src/traces/scattermapbox');
  var convert = require('@src/traces/scattermapbox/convert');
  
  var createGraphDiv = require('../assets/create_graph_div');
  var destroyGraphDiv = require('../assets/destroy_graph_div');
  var customMatchers = require('../assets/custom_matchers');
  
  var mouseEvent = require('../assets/mouse_event');
  var click = require('../assets/click');
  var HOVERMINTIME = require('@src/components/fx').constants.HOVERMINTIME;
  
  function move(fromX, fromY, toX, toY, delay) {
      return new Promise(function(resolve) {
          mouseEvent('mousemove', fromX, fromY);
  
          setTimeout(function() {
              mouseEvent('mousemove', toX, toY);
              resolve();
          }, delay || HOVERMINTIME + 10);
      });
  }
  
  describe('scattermapbox defaults', function() {
      'use strict';
  
      function _supply(traceIn) {
          var traceOut = { visible: true },
              defaultColor = '#444',
              layout = { _dataLength: 1 };
  
          ScatterMapbox.supplyDefaults(traceIn, traceOut, defaultColor, layout);
  
          return traceOut;
      }
  
      it('should truncate \'lon\' if longer than \'lat\'', function() {
          var fullTrace = _supply({
              lon: [1, 2, 3],
              lat: [2, 3]
          });
  
          expect(fullTrace.lon).toEqual([1, 2]);
          expect(fullTrace.lat).toEqual([2, 3]);
      });
  
      it('should truncate \'lat\' if longer than \'lon\'', function() {
          var fullTrace = _supply({
              lon: [1, 2, 3],
              lat: [2, 3, 3, 5]
          });
  
          expect(fullTrace.lon).toEqual([1, 2, 3]);
          expect(fullTrace.lat).toEqual([2, 3, 3]);
      });
  
      it('should set \'visible\' to false if \'lat\' and/or \'lon\' has zero length', function() {
          var fullTrace = _supply({
              lon: [1, 2, 3],
              lat: []
          });
  
          expect(fullTrace.visible).toEqual(false);
  
          fullTrace = _supply({
              lon: null,
              lat: [1, 2, 3]
          });
  
          expect(fullTrace.visible).toEqual(false);
      });
  
      it('should set \'marker.color\' and \'marker.size\' to first item if symbol is set to \'circle\'', function() {
          var base = {
              mode: 'markers',
              lon: [1, 2, 3],
              lat: [2, 3, 3],
              marker: {
                  color: ['red', 'green', 'blue'],
                  size: [10, 20, 30]
              }
          };
  
          var fullTrace = _supply(Lib.extendDeep({}, base, {
              marker: { symbol: 'monument' }
          }));
  
          expect(fullTrace.marker.color).toEqual('red');
          expect(fullTrace.marker.size).toEqual(10);
  
          fullTrace = _supply(Lib.extendDeep({}, base, {
              marker: { symbol: ['monument', 'music', 'harbor'] }
          }));
  
          expect(fullTrace.marker.color).toEqual('red');
          expect(fullTrace.marker.size).toEqual(10);
  
          fullTrace = _supply(Lib.extendDeep({}, base, {
              marker: { symbol: 'circle' }
          }));
  
          expect(fullTrace.marker.color).toEqual(['red', 'green', 'blue']);
          expect(fullTrace.marker.size).toEqual([10, 20, 30]);
      });
  });
  
  describe('scattermapbox convert', function() {
      'use strict';
  
      beforeAll(function() {
          jasmine.addMatchers(customMatchers);
      });
  
      function _convert(trace, selected) {
          var gd = { data: [trace] };
          Plots.supplyDefaults(gd);
  
          var fullTrace = gd._fullData[0];
          Plots.doCalcdata(gd, fullTrace);
  
          var calcTrace = gd.calcdata[0];
  
          if(selected) {
              var hasDimmedPts = false;
  
              selected.forEach(function(v, i) {
                  if(v) hasDimmedPts = true;
                  calcTrace[i].dim = v;
              });
  
              fullTrace._hasDimmedPts = hasDimmedPts;
          }
  
          return convert(calcTrace);
      }
  
      var base = {
          type: 'scattermapbox',
          lon: [10, '20', 30, 20, null, 20, 10],
          lat: [20, 20, '10', null, 10, 10, 20]
      };
  
      it('should generate correct output for markers + circle bubbles traces', function() {
          var opts = _convert(Lib.extendFlat({}, base, {
              mode: 'markers',
              marker: {
                  symbol: 'circle',
                  size: [10, 20, null, 10, '10'],
                  color: [10, null, '30', 20, 10]
              }
          }));
  
          assertVisibility(opts, ['none', 'none', 'visible', 'none']);
  
          expect(opts.circle.paint['circle-color']).toEqual({
              property: 'circle-color',
              stops: [
                  [0, 'rgb(220, 220, 220)'], [1, '#444'], [2, 'rgb(178, 10, 28)']
              ]
          }, 'circle-color stops');
  
          expect(opts.circle.paint['circle-radius']).toEqual({
              property: 'circle-radius',
              stops: [ [0, 5], [1, 10], [2, 0] ]
          }, 'circle-radius stops');
  
          expect(opts.circle.paint['circle-opacity']).toBe(0.7, 'circle-opacity');
  
          var circleProps = opts.circle.geojson.features.map(function(f) {
              return f.properties;
          });
  
          // N.B repeated values have same geojson props
          expect(circleProps).toEqual([
              { 'circle-color': 0, 'circle-radius': 0 },
              { 'circle-color': 1, 'circle-radius': 1 },
              { 'circle-color': 2, 'circle-radius': 2 },
              { 'circle-color': 1, 'circle-radius': 2 },
              { 'circle-color': 1, 'circle-radius': 2 }
          ], 'geojson feature properties');
      });
  
      it('should fill circle-opacity correctly', function() {
          var opts = _convert(Lib.extendFlat({}, base, {
              mode: 'markers',
              marker: {
                  symbol: 'circle',
                  size: 10,
                  color: 'red',
                  opacity: [1, null, 0.5, '0.5', '1', 0, 0.8]
              },
              opacity: 0.5
          }));
  
          assertVisibility(opts, ['none', 'none', 'visible', 'none']);
          expect(opts.circle.paint['circle-color']).toBe('red', 'circle-color');
          expect(opts.circle.paint['circle-radius']).toBe(5, 'circle-radius');
  
          expect(opts.circle.paint['circle-opacity']).toEqual({
              property: 'circle-opacity',
              stops: [ [0, 0.5], [1, 0], [2, 0.25], [6, 0.4] ]
          }, 'circle-opacity stops');
  
          var circleProps = opts.circle.geojson.features.map(function(f) {
              return f.properties;
          });
  
  
          expect(circleProps).toEqual([
              { 'circle-opacity': 0 },
              { 'circle-opacity': 1 },
              { 'circle-opacity': 2 },
              // lat === null
              // lon === null
              { 'circle-opacity': 1 },
              { 'circle-opacity': 6 },
          ], 'geojson feature properties');
      });
  
      it('should fill circle-opacity correctly during selections', function() {
          var _base = {
              type: 'scattermapbox',
              mode: 'markers',
              lon: [-10, 30, 20],
              lat: [45, 90, 180],
              marker: {symbol: 'circle'}
          };
  
          var specs = [{
              patch: {},
              selected: [0, 1, 1],
              expected: {stops: [[0, 1], [1, 0.2]], props: [0, 1, 1]}
          }, {
              patch: {opacity: 0.5},
              selected: [0, 1, 1],
              expected: {stops: [[0, 0.5], [1, 0.1]], props: [0, 1, 1]}
          }, {
              patch: {
                  marker: {opacity: 0.6}
              },
              selected: [1, 0, 1],
              expected: {stops: [[0, 0.12], [1, 0.6]], props: [0, 1, 0]}
          }, {
              patch: {
                  marker: {opacity: [0.5, 1, 0.6]}
              },
              selected: [1, 0, 1],
              expected: {stops: [[0, 0.1], [1, 1], [2, 0.12]], props: [0, 1, 2]}
          }, {
              patch: {
                  marker: {opacity: [2, null, -0.6]}
              },
              selected: [1, 1, 1],
              expected: {stops: [[0, 0.2], [1, 0]], props: [0, 1, 1]}
          }];
  
          specs.forEach(function(s, i) {
              var msg0 = '- case ' + i + ' ';
              var opts = _convert(Lib.extendDeep({}, _base, s.patch), s.selected);
  
              expect(opts.circle.paint['circle-opacity'].stops)
                  .toEqual(s.expected.stops, msg0 + 'stops');
  
              var props = opts.circle.geojson.features.map(function(f) {
                  return f.properties['circle-opacity'];
              });
  
              expect(props).toEqual(s.expected.props, msg0 + 'props');
          });
      });
  
      it('should generate correct output for fill + markers + lines traces', function() {
          var opts = _convert(Lib.extendFlat({}, base, {
              mode: 'markers+lines',
              marker: { symbol: 'circle' },
              fill: 'toself'
          }));
  
          assertVisibility(opts, ['visible', 'visible', 'visible', 'none']);
  
          var segment1 = [[10, 20], [20, 20], [30, 10]],
              segment2 = [[20, 10], [10, 20]];
  
          var lineCoords = [segment1, segment2],
              fillCoords = [[segment1], [segment2]];
  
          expect(opts.line.geojson.coordinates).toEqual(lineCoords, 'line coords');
          expect(opts.fill.geojson.coordinates).toEqual(fillCoords, 'fill coords');
  
          var circleCoords = opts.circle.geojson.features.map(function(f) {
              return f.geometry.coordinates;
          });
  
          expect(circleCoords).toEqual([
              [10, 20], [20, 20], [30, 10], [20, 10], [10, 20]
          ], 'circle coords');
      });
  
      it('should generate correct output for markers + non-circle traces', function() {
          var opts = _convert(Lib.extendFlat({}, base, {
              mode: 'markers',
              marker: { symbol: 'monument' }
          }));
  
          assertVisibility(opts, ['none', 'none', 'none', 'visible']);
  
          var symbolProps = opts.symbol.geojson.features.map(function(f) {
              return [f.properties.symbol, f.properties.text];
          });
  
          var expected = opts.symbol.geojson.features.map(function() {
              return ['monument', ''];
          });
  
          expect(symbolProps).toEqual(expected, 'geojson properties');
      });
  
      it('should generate correct output for text + lines traces', function() {
          var opts = _convert(Lib.extendFlat({}, base, {
              mode: 'lines+text',
              connectgaps: true,
              text: ['A', 'B', 'C', 'D', 'E', 'F']
          }));
  
          assertVisibility(opts, ['none', 'visible', 'none', 'visible']);
  
          var lineCoords = [
              [10, 20], [20, 20], [30, 10], [20, 10], [10, 20]
          ];
  
          expect(opts.line.geojson.coordinates).toEqual(lineCoords, 'line coords');
  
          var actualText = opts.symbol.geojson.features.map(function(f) {
              return f.properties.text;
          });
  
          expect(actualText).toEqual(['A', 'B', 'C', 'F', undefined]);
      });
  
      it('should generate correct output for lines traces with trailing gaps', function() {
          var opts = _convert(Lib.extendFlat({}, base, {
              mode: 'lines',
              lon: [10, '20', 30, 20, null, 20, 10, null, null],
              lat: [20, 20, '10', null, 10, 10, 20, null]
          }));
  
          assertVisibility(opts, ['none', 'visible', 'none', 'none']);
  
          var lineCoords = [
              [[10, 20], [20, 20], [30, 10]],
              [[20, 10], [10, 20]]
          ];
  
          expect(opts.line.geojson.coordinates).toEqual(lineCoords, 'have correct line coords');
      });
  
      it('should correctly convert \'textposition\' to \'text-anchor\' and \'text-offset\'', function() {
          var specs = {
              'top left': ['top-right', [-0.65, -1.65]],
              'top center': ['top', [0, -1.65]],
              'top right': ['top-left', [0.65, -1.65]],
              'middle left': ['right', [-0.65, 0]],
              'middle center': ['center', [0, 0]],
              'middle right': ['left', [0.65, 0]],
              'bottom left': ['bottom-right', [-0.65, 1.65]],
              'bottom center': ['bottom', [0, 1.65]],
              'bottom right': ['bottom-left', [0.65, 1.65]]
          };
  
          Object.keys(specs).forEach(function(k) {
              var spec = specs[k];
  
              var opts = _convert(Lib.extendFlat({}, base, {
                  textposition: k,
                  mode: 'text+markers',
                  marker: { size: 15 },
                  text: ['A', 'B', 'C']
              }));
  
              expect([
                  opts.symbol.layout['text-anchor'],
                  opts.symbol.layout['text-offset']
              ]).toEqual(spec, '(case ' + k + ')');
          });
      });
  
      it('should generate correct output for markers + circle bubbles traces with repeated values', function() {
          var opts = _convert(Lib.extendFlat({}, base, {
              lon: ['-96.796988', '-81.379236', '-85.311819', ''],
              lat: ['32.776664', '28.538335', '35.047157', '' ],
              marker: { size: ['5', '49', '5', ''] }
          }));
  
          expect(opts.circle.paint['circle-radius'].stops)
              .toBeCloseTo2DArray([[0, 2.5], [1, 24.5]], 'no replicate stops');
  
          var radii = opts.circle.geojson.features.map(function(f) {
              return f.properties['circle-radius'];
          });
  
          expect(radii).toBeCloseToArray([0, 1, 0], 'link features to correct stops');
      });
  
      it('should generate correct output for traces with only blank points', function() {
          var opts = _convert(Lib.extendFlat({}, base, {
              mode: 'lines',
              lon: ['', null],
              lat: [null, ''],
              fill: 'toself'
          }));
  
          // not optimal, but doesn't break anything as mapbox-gl accepts empty
          // coordinate arrays
          assertVisibility(opts, ['visible', 'visible', 'none', 'none']);
  
          expect(opts.line.geojson.coordinates).toEqual([], 'line coords');
          expect(opts.fill.geojson.coordinates).toEqual([], 'fill coords');
      });
  
      function assertVisibility(opts, expectations) {
          var actual = ['fill', 'line', 'circle', 'symbol'].map(function(l) {
              return opts[l].layout.visibility;
          });
  
          expect(actual).toEqual(expectations, 'layer visibility');
      }
  });
  
  describe('@noCI scattermapbox hover', function() {
      'use strict';
  
      var hoverPoints = ScatterMapbox.hoverPoints;
  
      var gd;
  
      beforeAll(function(done) {
          jasmine.addMatchers(customMatchers);
  
          Plotly.setPlotConfig({
              mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN
          });
  
          gd = createGraphDiv();
  
          var data = [{
              type: 'scattermapbox',
              lon: [10, 20, 30],
              lat: [10, 20, 30],
              text: ['A', 'B', 'C']
          }];
  
          Plotly.plot(gd, data, { autosize: true }).then(done);
      });
  
      afterAll(function() {
          Plotly.purge(gd);
          destroyGraphDiv();
      });
  
      function getPointData(gd) {
          var cd = gd.calcdata,
              mapbox = gd._fullLayout.mapbox._subplot;
  
          return {
              index: false,
              distance: 20,
              cd: cd[0],
              trace: cd[0][0].trace,
              xa: mapbox.xaxis,
              ya: mapbox.yaxis
          };
      }
  
      it('should generate hover label info (base case)', function() {
          var xval = 11,
              yval = 11;
  
          var out = hoverPoints(getPointData(gd), xval, yval)[0];
  
          expect(out.index).toEqual(0);
          expect([out.x0, out.x1, out.y0, out.y1]).toBeCloseToArray([
              297.444, 299.444, 105.410, 107.410
          ]);
          expect(out.extraText).toEqual('(10°, 10°)<br>A');
          expect(out.color).toEqual('#1f77b4');
      });
  
      it('should skip over blank and non-string text items', function(done) {
          var xval = 11,
              yval = 11,
              out;
  
          Plotly.restyle(gd, 'text', [['', 'B', 'C']]).then(function() {
              out = hoverPoints(getPointData(gd), xval, yval)[0];
              expect(out.extraText).toEqual('(10°, 10°)');
  
              return Plotly.restyle(gd, 'text', [[null, 'B', 'C']]);
          })
          .then(function() {
              out = hoverPoints(getPointData(gd), xval, yval)[0];
              expect(out.extraText).toEqual('(10°, 10°)');
  
              return Plotly.restyle(gd, 'text', [[false, 'B', 'C']]);
          })
          .then(function() {
              out = hoverPoints(getPointData(gd), xval, yval)[0];
              expect(out.extraText).toEqual('(10°, 10°)');
  
              return Plotly.restyle(gd, 'text', [['A', 'B', 'C']]);
          })
          .then(function() {
              out = hoverPoints(getPointData(gd), xval, yval)[0];
              expect(out.extraText).toEqual('(10°, 10°)<br>A');
          })
          .then(done);
      });
  
      it('should generate hover label info (positive winding case)', function() {
          var xval = 11 + 720,
              yval = 11;
  
          var out = hoverPoints(getPointData(gd), xval, yval)[0];
  
          expect(out.index).toEqual(0);
          expect([out.x0, out.x1, out.y0, out.y1]).toBeCloseToArray([
              2345.444, 2347.444, 105.410, 107.410
          ]);
          expect(out.extraText).toEqual('(10°, 10°)<br>A');
          expect(out.color).toEqual('#1f77b4');
      });
  
      it('should generate hover label info (negative winding case)', function() {
          var xval = 11 - 1080,
              yval = 11;
  
          var out = hoverPoints(getPointData(gd), xval, yval)[0];
  
          expect(out.index).toEqual(0);
          expect([out.x0, out.x1, out.y0, out.y1]).toBeCloseToArray([
              -2774.555, -2772.555, 105.410, 107.410
          ]);
          expect(out.extraText).toEqual('(10°, 10°)<br>A');
          expect(out.color).toEqual('#1f77b4');
      });
  
      it('should generate hover label info (hoverinfo: \'lon\' case)', function(done) {
          Plotly.restyle(gd, 'hoverinfo', 'lon').then(function() {
              var xval = 11,
                  yval = 11;
  
              var out = hoverPoints(getPointData(gd), xval, yval)[0];
  
              expect(out.extraText).toEqual('lon: 10°');
              done();
          });
      });
  
      it('should generate hover label info (hoverinfo: \'lat\' case)', function(done) {
          Plotly.restyle(gd, 'hoverinfo', 'lat').then(function() {
              var xval = 11,
                  yval = 11;
  
              var out = hoverPoints(getPointData(gd), xval, yval)[0];
  
              expect(out.extraText).toEqual('lat: 10°');
              done();
          });
      });
  
      it('should generate hover label info (hoverinfo: \'text\' + \'text\' array case)', function(done) {
          Plotly.restyle(gd, 'hoverinfo', 'text').then(function() {
              var xval = 11,
                  yval = 11;
  
              var out = hoverPoints(getPointData(gd), xval, yval)[0];
  
              expect(out.extraText).toEqual('A');
              done();
          });
      });
  
      it('should generate hover label info (hoverinfo: \'text\' + \'hovertext\' array case)', function(done) {
          Plotly.restyle(gd, 'hovertext', ['Apple', 'Banana', 'Orange']).then(function() {
              var xval = 11,
                  yval = 11;
  
              var out = hoverPoints(getPointData(gd), xval, yval)[0];
  
              expect(out.extraText).toEqual('Apple');
              done();
          });
      });
  
      it('should generate hover label (\'marker.color\' array case)', function(done) {
          Plotly.restyle(gd, 'marker.color', [['red', 'blue', 'green']]).then(function() {
              var out = hoverPoints(getPointData(gd), 11, 11)[0];
  
              expect(out.color).toEqual('red');
          })
          .then(done);
      });
  
      it('should generate hover label (\'marker.color\' w/ colorscale case)', function(done) {
          Plotly.restyle(gd, 'marker.color', [[10, 5, 30]]).then(function() {
              var out = hoverPoints(getPointData(gd), 11, 11)[0];
  
              expect(out.color).toEqual('rgb(245, 195, 157)');
          })
          .then(done);
      });
  });
  
  describe('@noCI Test plotly events on a scattermapbox plot:', function() {
      var mock = require('@mocks/mapbox_0.json');
  
      var mockCopy, gd;
  
      var blankPos = [10, 10],
          pointPos,
          nearPos;
  
      function getPointData(gd) {
          var cd = gd.calcdata,
              mapbox = gd._fullLayout.mapbox._subplot;
  
          return {
              index: false,
              distance: 20,
              cd: cd[0],
              trace: cd[0][0].trace,
              xa: mapbox.xaxis,
              ya: mapbox.yaxis
          };
      }
  
      beforeAll(function(done) {
          jasmine.addMatchers(customMatchers);
  
          Plotly.setPlotConfig({
              mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN
          });
  
          gd = createGraphDiv();
          mockCopy = Lib.extendDeep({}, mock);
  
          Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() {
              var bb = gd._fullLayout.mapbox._subplot.div.getBoundingClientRect(),
                  xval = 10,
                  yval = 10,
                  point = ScatterMapbox.hoverPoints(getPointData(gd), xval, yval)[0];
              pointPos = [Math.floor(bb.left + (point.x0 + point.x1) / 2),
                  Math.floor(bb.top + (point.y0 + point.y1) / 2)];
              nearPos = [pointPos[0] - 30, pointPos[1] - 30];
          }).then(destroyGraphDiv).then(done);
      });
  
      beforeEach(function() {
          gd = createGraphDiv();
          mockCopy = Lib.extendDeep({}, mock);
      });
  
      afterEach(destroyGraphDiv);
  
      describe('click events', function() {
          var futureData;
  
          beforeEach(function(done) {
              Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done);
  
              gd.on('plotly_click', function(data) {
                  futureData = data;
              });
          });
  
          it('should not be trigged when not on data points', function() {
              click(blankPos[0], blankPos[1]);
              expect(futureData).toBe(undefined);
          });
  
          it('should contain the correct fields', function() {
              click(pointPos[0], pointPos[1]);
  
              var pt = futureData.points[0],
                  evt = futureData.event;
  
              expect(Object.keys(pt)).toEqual([
                  'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat'
              ]);
  
              expect(pt.curveNumber).toEqual(0, 'points[0].curveNumber');
              expect(typeof pt.data).toEqual(typeof {}, 'points[0].data');
              expect(typeof pt.fullData).toEqual(typeof {}, 'points[0].fullData');
              expect(pt.lat).toEqual(10, 'points[0].lat');
              expect(pt.lon).toEqual(10, 'points[0].lon');
              expect(pt.pointNumber).toEqual(0, 'points[0].pointNumber');
  
              expect(evt.clientX).toEqual(pointPos[0], 'event.clientX');
              expect(evt.clientY).toEqual(pointPos[1], 'event.clientY');
          });
      });
  
      describe('modified click events', function() {
          var clickOpts = {
                  altKey: true,
                  ctrlKey: true,
                  metaKey: true,
                  shiftKey: true
              },
              futureData;
  
          beforeEach(function(done) {
              Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done);
  
              gd.on('plotly_click', function(data) {
                  futureData = data;
              });
          });
  
          it('should not be trigged when not on data points', function() {
              click(blankPos[0], blankPos[1], clickOpts);
              expect(futureData).toBe(undefined);
          });
  
          it('should contain the correct fields', function() {
              click(pointPos[0], pointPos[1], clickOpts);
  
              var pt = futureData.points[0],
                  evt = futureData.event;
  
              expect(Object.keys(pt)).toEqual([
                  'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat'
              ]);
  
              expect(pt.curveNumber).toEqual(0, 'points[0].curveNumber');
              expect(typeof pt.data).toEqual(typeof {}, 'points[0].data');
              expect(typeof pt.fullData).toEqual(typeof {}, 'points[0].fullData');
              expect(pt.lat).toEqual(10, 'points[0].lat');
              expect(pt.lon).toEqual(10, 'points[0].lon');
              expect(pt.pointNumber).toEqual(0, 'points[0].pointNumber');
  
              expect(evt.clientX).toEqual(pointPos[0], 'event.clientX');
              expect(evt.clientY).toEqual(pointPos[1], 'event.clientY');
              Object.getOwnPropertyNames(clickOpts).forEach(function(opt) {
                  expect(evt[opt]).toEqual(clickOpts[opt], 'event.' + opt);
              });
          });
      });
  
      describe('hover events', function() {
          var futureData;
  
          beforeEach(function(done) {
              Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done);
  
              gd.on('plotly_hover', function(data) {
                  futureData = data;
              });
          });
  
          it('should contain the correct fields', function() {
              mouseEvent('mousemove', blankPos[0], blankPos[1]);
              mouseEvent('mousemove', pointPos[0], pointPos[1]);
  
              var pt = futureData.points[0],
                  evt = futureData.event;
  
              expect(Object.keys(pt)).toEqual([
                  'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat'
              ]);
  
              expect(pt.curveNumber).toEqual(0, 'points[0].curveNumber');
              expect(typeof pt.data).toEqual(typeof {}, 'points[0].data');
              expect(typeof pt.fullData).toEqual(typeof {}, 'points[0].fullData');
              expect(pt.lat).toEqual(10, 'points[0].lat');
              expect(pt.lon).toEqual(10, 'points[0].lon');
              expect(pt.pointNumber).toEqual(0, 'points[0].pointNumber');
  
              expect(evt.clientX).toEqual(pointPos[0], 'event.clientX');
              expect(evt.clientY).toEqual(pointPos[1], 'event.clientY');
          });
      });
  
      describe('unhover events', function() {
          var futureData;
  
          beforeEach(function(done) {
              Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done);
  
              gd.on('plotly_unhover', function(data) {
                  futureData = data;
              });
          });
  
          it('should contain the correct fields', function(done) {
              move(pointPos[0], pointPos[1], nearPos[0], nearPos[1], HOVERMINTIME + 10).then(function() {
                  var pt = futureData.points[0],
                      evt = futureData.event;
  
                  expect(Object.keys(pt)).toEqual([
                      'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat'
                  ]);
  
                  expect(pt.curveNumber).toEqual(0, 'points[0].curveNumber');
                  expect(typeof pt.data).toEqual(typeof {}, 'points[0].data');
                  expect(typeof pt.fullData).toEqual(typeof {}, 'points[0].fullData');
                  expect(pt.lat).toEqual(10, 'points[0].lat');
                  expect(pt.lon).toEqual(10, 'points[0].lon');
                  expect(pt.pointNumber).toEqual(0, 'points[0].pointNumber');
  
                  expect(evt.clientX).toEqual(nearPos[0], 'event.clientX');
                  expect(evt.clientY).toEqual(nearPos[1], 'event.clientY');
              }).then(done);
          });
      });
  });
  