<commit_before>/* eslint-disable react/no-multi-comp */
import React from 'react';
import ReactDOM from 'react-dom';
import Waypoint from '../src/waypoint.jsx';

import { errorMessage as notValidErrorMessage } from '../src/ensureChildrenIsValid';
import { errorMessage as refNotUsedErrorMessage } from '../src/ensureRefIsUsedByChild';

let div;

const renderAttached = function(component) {
  div = document.createElement('div');
  document.body.appendChild(div);
  const renderedComponent = ReactDOM.render(component, div);
  return renderedComponent;
};

const scrollNodeTo = function(node, scrollTop) {
  if (node === window) {
    window.scroll(0, scrollTop);
  } else {
    node.scrollTop = scrollTop;
  }
  const event = document.createEvent('Event');
  event.initEvent('scroll', false, false);
  node.dispatchEvent(event);
};

describe('<Waypoint>', function() {
  beforeEach(() => {
    jasmine.clock().install();
    spyOn(console, 'log');
    this.props = {
      onEnter: jasmine.createSpy('onEnter'),
      onLeave: jasmine.createSpy('onLeave'),
      onPositionChange: jasmine.createSpy('onPositionChange'),
    };

    this.margin = 10;
    this.parentHeight = 100;

    this.parentStyle = {
      height: this.parentHeight,
      overflow: 'auto',
      position: 'relative',
      width: 100,
      margin: this.margin, //Normalize the space above the viewport.
    };

    this.topSpacerHeight = 0;
    this.bottomSpacerHeight = 0;

    this.subject = () => {
      const el = renderAttached(
        <div style={this.parentStyle}>
          <div style={{ height: this.topSpacerHeight }} />
          <Waypoint {...this.props} />
          <div style={{ height: this.bottomSpacerHeight }} />
        </div>
      );

      jasmine.clock().tick(1);
      return el;
    };
  });

  afterEach(() => {
    if (div) {
      ReactDOM.unmountComponentAtNode(div);
    }
    scrollNodeTo(window, 0);
    jasmine.clock().uninstall();
  });

  it('logs to the console when called with debug = true', () => {
    this.props.debug = true;
    this.subject();
    expect(console.log).toHaveBeenCalled(); // eslint-disable-line no-console
  });

  describe('when the Waypoint is visible on mount', () => {
    beforeEach(() => {
      this.topSpacerHeight = 90;
      this.bottomSpacerHeight = 200;
      this.parentComponent = this.subject();
      this.scrollable = this.parentComponent;
    });

    it('does not log to the console', () => {
      // eslint-disable-next-line no-console
      expect(console.log).not.toHaveBeenCalled();
    });

    it('calls the onEnter handler', () => {
      expect(this.props.onEnter).toHaveBeenCalledWith({
        currentPosition: Waypoint.inside,
        previousPosition: undefined,
        event: null,
        waypointTop: this.margin + this.topSpacerHeight,
        waypointBottom: this.margin + this.topSpacerHeight,
        viewportTop: this.margin,
        viewportBottom: this.margin + this.parentHeight,
      });
    });

    it('calls the onPositionChange handler', () => {
      expect(this.props.onPositionChange).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.inside,
          previousPosition: undefined,
          event: null,
          waypointTop: this.margin + this.topSpacerHeight,
          waypointBottom: this.margin + this.topSpacerHeight,
          viewportTop: this.margin,
          viewportBottom: this.margin + this.parentHeight,
        });
    });

    it('does not call the onLeave handler', () => {
      expect(this.props.onLeave).not.toHaveBeenCalled();
    });
  });

  describe('when the Waypoint is visible on mount and has topOffset < -100%', () => {
    beforeEach(() => {
      this.props.topOffset = '-200%';

      this.topSpacerHeight = 90;
      this.bottomSpacerHeight = 200;
      this.parentComponent = this.subject();
      this.scrollable = this.parentComponent;
    });

    it('calls the onEnter handler', () => {
      expect(this.props.onEnter).toHaveBeenCalledWith({
        currentPosition: Waypoint.inside,
        previousPosition: undefined,
        event: null,
        waypointTop: this.margin + this.topSpacerHeight,
        waypointBottom: this.margin + this.topSpacerHeight,
        viewportTop: this.margin - (this.parentHeight * 2),
        viewportBottom: this.margin + this.parentHeight,
      });
    });

    it('calls the onPositionChange handler', () => {
      expect(this.props.onPositionChange).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.inside,
          previousPosition: undefined,
          event: null,
          waypointTop: this.margin + this.topSpacerHeight,
          waypointBottom: this.margin + this.topSpacerHeight,
          viewportTop: this.margin - (this.parentHeight * 2),
          viewportBottom: this.margin + this.parentHeight,
        });
    });

    it('does not call the onLeave handler', () => {
      expect(this.props.onLeave).not.toHaveBeenCalled();
    });
  });

  describe('when the Waypoint is visible on mount and has bottomOffset < -100%', () => {
    beforeEach(() => {
      this.props.bottomOffset = '-200%';

      this.topSpacerHeight = 90;
      this.bottomSpacerHeight = 200;
      this.parentComponent = this.subject();
      this.scrollable = this.parentComponent;
    });

    it('calls the onEnter handler', () => {
      expect(this.props.onEnter).toHaveBeenCalledWith({
        currentPosition: Waypoint.inside,
        previousPosition: undefined,
        event: null,
        waypointTop: this.margin + this.topSpacerHeight,
        waypointBottom: this.margin + this.topSpacerHeight,
        viewportTop: this.margin,
        viewportBottom: this.margin + (this.parentHeight * 3),
      });
    });

    it('calls the onPositionChange handler', () => {
      expect(this.props.onPositionChange).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.inside,
          previousPosition: undefined,
          event: null,
          waypointTop: this.margin + this.topSpacerHeight,
          waypointBottom: this.margin + this.topSpacerHeight,
          viewportTop: this.margin,
          viewportBottom: this.margin + (this.parentHeight * 3),
        });
    });

    it('does not call the onLeave handler', () => {
      expect(this.props.onLeave).not.toHaveBeenCalled();
    });
  });

  describe('when the Waypoint is visible on mount and offsets < -100%', () => {
    beforeEach(() => {
      this.props.topOffset = '-200%';
      this.props.bottomOffset = '-200%';

      this.topSpacerHeight = 90;
      this.bottomSpacerHeight = 200;
      this.parentComponent = this.subject();
      this.scrollable = this.parentComponent;
    });

    it('calls the onEnter handler', () => {
      expect(this.props.onEnter).toHaveBeenCalledWith({
        currentPosition: Waypoint.inside,
        previousPosition: undefined,
        event: null,
        waypointTop: this.margin + this.topSpacerHeight,
        waypointBottom: this.margin + this.topSpacerHeight,
        viewportTop: this.margin - (this.parentHeight * 2),
        viewportBottom: this.margin + (this.parentHeight * 3),
      });
    });

    it('calls the onPositionChange handler', () => {
      expect(this.props.onPositionChange).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.inside,
          previousPosition: undefined,
          event: null,
          waypointTop: this.margin + this.topSpacerHeight,
          waypointBottom: this.margin + this.topSpacerHeight,
          viewportTop: this.margin - (this.parentHeight * 2),
          viewportBottom: this.margin + (this.parentHeight * 3),
        });
    });

    it('does not call the onLeave handler', () => {
      expect(this.props.onLeave).not.toHaveBeenCalled();
    });
  });

  describe('when scrolling while the waypoint is visible', () => {
    beforeEach(() => {
      this.topSpacerHeight = 90;
      this.bottomSpacerHeight = 200;
      this.parentComponent = this.subject();
      this.scrollable = this.parentComponent;
      scrollNodeTo(this.scrollable, this.topSpacerHeight / 2);
    });

    it('does not call the onEnter handler again', () => {
      expect(this.props.onEnter.calls.count()).toBe(1);
    });

    it('does not call the onLeave handler', () => {
      expect(this.props.onLeave).not.toHaveBeenCalled();
    });

    it('does not call the onPositionChange handler again', () => {
      expect(this.props.onPositionChange.calls.count()).toBe(1);
    });
  });

  describe('when scrolling past the waypoint while it is visible', () => {
    beforeEach(() => {
      this.topSpacerHeight = 90;
      this.bottomSpacerHeight = 200;
      this.parentComponent = this.subject();
      this.scrollable = this.parentComponent;
      scrollNodeTo(this.scrollable, this.topSpacerHeight + 10);
    });

    it('the onLeave handler is called', () => {
      expect(this.props.onLeave).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.above,
          previousPosition: Waypoint.inside,
          event: jasmine.any(Event),
          waypointTop: this.margin - 10,
          waypointBottom: this.margin - 10,
          viewportTop: this.margin,
          viewportBottom: this.margin + this.parentHeight,
        });
    });

    it('does not call the onEnter handler', () => {
      expect(this.props.onEnter.calls.count()).toBe(1);
    });

    it('the onPositionChange is called', () => {
      expect(this.props.onPositionChange).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.above,
          previousPosition: Waypoint.inside,
          event: jasmine.any(Event),
          waypointTop: this.margin - 10,
          waypointBottom: this.margin - 10,
          viewportTop: this.margin,
          viewportBottom: this.margin + this.parentHeight,
        });
    });
  });

  describe('when the Waypoint is below the bottom', () => {
    beforeEach(() => {
      this.topSpacerHeight = 200;

      // The bottom spacer needs to be tall enough to force the Waypoint to exit
      // the viewport when scrolled all the way down.
      this.bottomSpacerHeight = 3000;
    });

    it('does not call the onEnter handler on mount', () => {
      this.subject();
      expect(this.props.onEnter).not.toHaveBeenCalled();
    });

    it('does not call the onLeave handler on mount', () => {
      this.subject();
      expect(this.props.onLeave).not.toHaveBeenCalled();
    });

    it('calls the onPositionChange handler', () => {
      this.subject();
      expect(this.props.onPositionChange).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.below,
          previousPosition: undefined,
          event: null,
          waypointTop: this.margin + this.topSpacerHeight,
          waypointBottom: this.margin + this.topSpacerHeight,
          viewportTop: this.margin,
          viewportBottom: this.margin + this.parentHeight,
        });
    });

    describe('with children', () => {
      beforeEach(() => {
        this.childrenHeight = 80;
        this.props.children = (
          <div>
            <div style={{ height: this.childrenHeight / 2 }} />
            <div style={{ height: this.childrenHeight / 2 }} />
          </div>
        );
      });

      it('calls the onEnter handler when scrolling down far enough', () => {
        this.component = this.subject();
        this.props.onPositionChange.calls.reset();
        scrollNodeTo(this.component, 100);

        expect(this.props.onEnter).
          toHaveBeenCalledWith({
            currentPosition: Waypoint.inside,
            previousPosition: Waypoint.below,
            event: jasmine.any(Event),
            waypointTop: this.margin + this.topSpacerHeight - 100,
            waypointBottom: this.margin + this.topSpacerHeight - 100 + this.childrenHeight,
            viewportTop: this.margin,
            viewportBottom: this.margin + this.parentHeight,
          });
      });
    });

    describe('when scrolling down just below the threshold', () => {
      beforeEach(() => {
        this.component = this.subject();
        this.props.onPositionChange.calls.reset();
        scrollNodeTo(this.component, 99);
      });

      it('does not call the onEnter handler', () => {
        expect(this.props.onEnter).not.toHaveBeenCalled();
      });

      it('does not call the onLeave handler', () => {
        expect(this.props.onLeave).not.toHaveBeenCalled();
      });

      it('does not call the onPositionChange handler', () => {
        expect(this.props.onPositionChange).not.toHaveBeenCalled();
      });
    });

    it('calls the onEnter handler when scrolling down past the threshold', () => {
      scrollNodeTo(this.subject(), 100);

      expect(this.props.onEnter).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.inside,
          previousPosition: Waypoint.below,
          event: jasmine.any(Event),
          waypointTop: this.margin + this.topSpacerHeight - 100,
          waypointBottom: this.margin + this.topSpacerHeight - 100,
          viewportTop: this.margin,
          viewportBottom: this.margin + this.parentHeight,
        });
    });

    it('calls the onPositionChange handler when scrolling down past the threshold', () => {
      scrollNodeTo(this.subject(), 100);

      expect(this.props.onPositionChange).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.inside,
          previousPosition: Waypoint.below,
          event: jasmine.any(Event),
          waypointTop: this.margin + this.topSpacerHeight - 100,
          waypointBottom: this.margin + this.topSpacerHeight - 100,
          viewportTop: this.margin,
          viewportBottom: this.margin + this.parentHeight,
        });
    });

    it('does not call the onLeave handler when scrolling down past the threshold', () => {
      scrollNodeTo(this.subject(), 100);
      expect(this.props.onLeave).not.toHaveBeenCalled();
    });

    describe('when `fireOnRapidScroll` is disabled', () => {
      beforeEach(() => {
        this.props.fireOnRapidScroll = false;
      });

      it('calls the onEnter handler when scrolling down past the threshold', () => {
        scrollNodeTo(this.subject(), 100);

        expect(this.props.onEnter).
          toHaveBeenCalledWith({
            currentPosition: Waypoint.inside,
            previousPosition: Waypoint.below,
            event: jasmine.any(Event),
            waypointTop: this.margin + this.topSpacerHeight - 100,
            waypointBottom: this.margin + this.topSpacerHeight - 100,
            viewportTop: this.margin,
            viewportBottom: this.margin + this.parentHeight,
          });
      });

      it('calls the onPositionChange handler when scrolling down past the threshold', () => {
        scrollNodeTo(this.subject(), 100);

        expect(this.props.onPositionChange).
          toHaveBeenCalledWith({
            currentPosition: Waypoint.inside,
            previousPosition: Waypoint.below,
            event: jasmine.any(Event),
            waypointTop: this.margin + this.topSpacerHeight - 100,
            waypointBottom: this.margin + this.topSpacerHeight - 100,
            viewportTop: this.margin,
            viewportBottom: this.margin + this.parentHeight,
          });
      });

      it('does not call the onLeave handler when scrolling down past the threshold', () => {
        scrollNodeTo(this.subject(), 100);

        expect(this.props.onLeave).not.toHaveBeenCalled();
      });
    });

    describe('when scrolling quickly past the waypoint', () => {
      // If you scroll really fast, we might not get a scroll event when the
      // waypoint is in view. We will get a scroll event before going into view
      // though, and one after. We want to treat this as if the waypoint was
      // visible for a brief moment, and so we fire both onEnter and onLeave.
      beforeEach(() => {
        this.scrollQuicklyPast = () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, this.topSpacerHeight + this.bottomSpacerHeight);
        };
      });

      it('calls the onEnter handler', () => {
        this.scrollQuicklyPast();
        expect(this.props.onEnter).
          toHaveBeenCalledWith({
            currentPosition: Waypoint.inside,
            previousPosition: Waypoint.below,
            event: jasmine.any(Event),
            waypointTop: this.margin - this.bottomSpacerHeight + this.parentHeight,
            waypointBottom: this.margin - this.bottomSpacerHeight + this.parentHeight,
            viewportTop: this.margin,
            viewportBottom: this.margin + this.parentHeight,
          });
      });

      it('calls the onLeave handler', () => {
        this.scrollQuicklyPast();
        expect(this.props.onLeave).
          toHaveBeenCalledWith({
            currentPosition: Waypoint.above,
            previousPosition: Waypoint.inside,
            event: jasmine.any(Event),
            waypointTop: this.margin - this.bottomSpacerHeight + this.parentHeight,
            waypointBottom: this.margin - this.bottomSpacerHeight + this.parentHeight,
            viewportTop: this.margin,
            viewportBottom: this.margin + this.parentHeight,
          });
      });

      it('calls the onPositionChange handler', () => {
        this.scrollQuicklyPast();
        expect(this.props.onPositionChange).
          toHaveBeenCalledWith({
            currentPosition: Waypoint.above,
            previousPosition: Waypoint.below,
            event: jasmine.any(Event),
            waypointTop: this.margin - this.bottomSpacerHeight + this.parentHeight,
            waypointBottom: this.margin - this.bottomSpacerHeight + this.parentHeight,
            viewportTop: this.margin,
            viewportBottom: this.margin + this.parentHeight,
          });
      });

      describe('when `fireOnRapidScroll` is disabled', () => {
        beforeEach(() => {
          this.props.fireOnRapidScroll = false;
        });

        it('does not call the onEnter handler', () => {
          this.scrollQuicklyPast();
          expect(this.props.onEnter).not.toHaveBeenCalled();
        });

        it('does not call the onLeave handler', () => {
          this.scrollQuicklyPast();
          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('calls the onPositionChange handler', () => {
          this.scrollQuicklyPast();
          expect(this.props.onPositionChange).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.above,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin - this.bottomSpacerHeight + this.parentHeight,
              waypointBottom: this.margin - this.bottomSpacerHeight + this.parentHeight,
              viewportTop: this.margin,
              viewportBottom: this.margin + this.parentHeight,
            });
        });
      });
    });

    describe('with a non-zero topOffset', () => {
      describe('and the topOffset is passed as a percentage', () => {
        beforeEach(() => {
          this.props.topOffset = '-10%';
        });

        it('calls the onLeave handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 211);

          expect(this.props.onLeave).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.above,
              previousPosition: Waypoint.inside,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 211,
              waypointBottom: this.margin + this.topSpacerHeight - 211,
              viewportTop: this.margin + this.parentHeight * -0.1,
              viewportBottom: this.margin + this.parentHeight,
            });
        });
      });
    });

    describe('with a non-zero bottomOffset', () => {
      describe('and the bottomOffset is passed as a percentage', () => {
        beforeEach(() => {
          this.props.bottomOffset = '-10%';
        });

        it('does not call the onEnter handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onEnter).not.toHaveBeenCalled();
        });

        it('does not call the onLeave handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('does not call onPositionChange handler when scrolling down near bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onPositionChange).not.toHaveBeenCalled();
        });

        it('calls the onEnter handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onEnter).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + Math.floor(this.parentHeight * 1.1),
            });
        });

        it('does not call the onLeave handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('calls the onPositionChange handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onPositionChange).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + Math.floor(this.parentHeight * 1.1),
            });
        });
      });

      describe('and the bottom offset is passed as a numeric string', () => {
        beforeEach(() => {
          this.props.bottomOffset = '-10';
        });

        it('does not call the onEnter handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onEnter).not.toHaveBeenCalled();
        });

        it('does not call the onLeave handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('does not call onPositionChange handler when scrolling down near bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onPositionChange).not.toHaveBeenCalled();
        });

        it('calls the onEnter handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onEnter).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + this.parentHeight + 10,
            });
        });

        it('does not call the onLeave handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('calls the onPositionChange handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onPositionChange).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + this.parentHeight + 10,
            });
        });
      });

      describe('and the bottom offset is passed as a pixel string', () => {
        beforeEach(() => {
          this.props.bottomOffset = '-10px';
        });

        it('does not call the onEnter handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onEnter).not.toHaveBeenCalled();
        });

        it('does not call the onLeave handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('does not call onPositionChange handler when scrolling down near bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onPositionChange).not.toHaveBeenCalled();
        });

        it('calls the onEnter handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onEnter).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + this.parentHeight + 10,
            });
        });

        it('does not call the onLeave handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('calls the onPositionChange handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onPositionChange).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + this.parentHeight + 10,
            });
        });
      });

      describe('and the bottom offset is passed as a number', () => {
        beforeEach(() => {
          this.props.bottomOffset = -10;
        });

        it('does not call the onEnter handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onEnter).not.toHaveBeenCalled();
        });

        it('does not call the onLeave handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('does not call onPositionChange handler when scrolling down near bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onPositionChange).not.toHaveBeenCalled();
        });

        it('calls the onEnter handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onEnter).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + this.parentHeight + 10,
            });
        });

        it('does not call the onLeave handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('calls onPositionChange handler when scrolling down past bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onPositionChange).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + this.parentHeight + 10,
            });
        });
      });
    });
  });

  describe('when the Waypoint has children', () => {
    it('does not throw with a DOM Element as a child', () => {
      this.props.children = <div />;
      expect(this.subject).not.toThrow();
    });

    it('errors when multiple children are provided', (done) => {
      this.props.children = [
        <div key={1} />,
        <div key={2} />,
        <div key={3} />,
      ];

      window.onerror = () => {
        // INFO: React 16 throws user errorrs again by it's own in dev mode, see:
        // https://github.com/facebook/react/blob/e932ad68bed656eed5295b61ba74e5d0857902ed/src/renderers/shared/fiber/ReactFiberErrorLogger.js#L65
        //
        // This hack prevents tests from failing after uncaught exception is thrown by React
        done();
      };

      expect(this.subject).toThrowError(notValidErrorMessage);
    });

    it('does not throw with a Stateful Component as a child', () => {
      class StatefulComponent extends React.Component {
        render() {
          return <div ref={this.props.innerRef} />;
        }
      }

      this.props.children = <StatefulComponent />;
      expect(this.subject).not.toThrow();
    });
<commit_msg>Fix test for react < 16<commit_after>/* eslint-disable react/no-multi-comp */
import React from 'react';
import ReactDOM from 'react-dom';
import Waypoint from '../src/waypoint.jsx';

import { errorMessage as notValidErrorMessage } from '../src/ensureChildrenIsValid';
import { errorMessage as refNotUsedErrorMessage } from '../src/ensureRefIsUsedByChild';

let div;

const renderAttached = function(component) {
  div = document.createElement('div');
  document.body.appendChild(div);
  const renderedComponent = ReactDOM.render(component, div);
  return renderedComponent;
};

const scrollNodeTo = function(node, scrollTop) {
  if (node === window) {
    window.scroll(0, scrollTop);
  } else {
    node.scrollTop = scrollTop;
  }
  const event = document.createEvent('Event');
  event.initEvent('scroll', false, false);
  node.dispatchEvent(event);
};

describe('<Waypoint>', function() {
  beforeEach(() => {
    jasmine.clock().install();
    spyOn(console, 'log');
    this.props = {
      onEnter: jasmine.createSpy('onEnter'),
      onLeave: jasmine.createSpy('onLeave'),
      onPositionChange: jasmine.createSpy('onPositionChange'),
    };

    this.margin = 10;
    this.parentHeight = 100;

    this.parentStyle = {
      height: this.parentHeight,
      overflow: 'auto',
      position: 'relative',
      width: 100,
      margin: this.margin, //Normalize the space above the viewport.
    };

    this.topSpacerHeight = 0;
    this.bottomSpacerHeight = 0;

    this.subject = () => {
      const el = renderAttached(
        <div style={this.parentStyle}>
          <div style={{ height: this.topSpacerHeight }} />
          <Waypoint {...this.props} />
          <div style={{ height: this.bottomSpacerHeight }} />
        </div>
      );

      jasmine.clock().tick(1);
      return el;
    };
  });

  afterEach(() => {
    if (div) {
      ReactDOM.unmountComponentAtNode(div);
    }
    scrollNodeTo(window, 0);
    jasmine.clock().uninstall();
  });

  it('logs to the console when called with debug = true', () => {
    this.props.debug = true;
    this.subject();
    expect(console.log).toHaveBeenCalled(); // eslint-disable-line no-console
  });

  describe('when the Waypoint is visible on mount', () => {
    beforeEach(() => {
      this.topSpacerHeight = 90;
      this.bottomSpacerHeight = 200;
      this.parentComponent = this.subject();
      this.scrollable = this.parentComponent;
    });

    it('does not log to the console', () => {
      // eslint-disable-next-line no-console
      expect(console.log).not.toHaveBeenCalled();
    });

    it('calls the onEnter handler', () => {
      expect(this.props.onEnter).toHaveBeenCalledWith({
        currentPosition: Waypoint.inside,
        previousPosition: undefined,
        event: null,
        waypointTop: this.margin + this.topSpacerHeight,
        waypointBottom: this.margin + this.topSpacerHeight,
        viewportTop: this.margin,
        viewportBottom: this.margin + this.parentHeight,
      });
    });

    it('calls the onPositionChange handler', () => {
      expect(this.props.onPositionChange).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.inside,
          previousPosition: undefined,
          event: null,
          waypointTop: this.margin + this.topSpacerHeight,
          waypointBottom: this.margin + this.topSpacerHeight,
          viewportTop: this.margin,
          viewportBottom: this.margin + this.parentHeight,
        });
    });

    it('does not call the onLeave handler', () => {
      expect(this.props.onLeave).not.toHaveBeenCalled();
    });
  });

  describe('when the Waypoint is visible on mount and has topOffset < -100%', () => {
    beforeEach(() => {
      this.props.topOffset = '-200%';

      this.topSpacerHeight = 90;
      this.bottomSpacerHeight = 200;
      this.parentComponent = this.subject();
      this.scrollable = this.parentComponent;
    });

    it('calls the onEnter handler', () => {
      expect(this.props.onEnter).toHaveBeenCalledWith({
        currentPosition: Waypoint.inside,
        previousPosition: undefined,
        event: null,
        waypointTop: this.margin + this.topSpacerHeight,
        waypointBottom: this.margin + this.topSpacerHeight,
        viewportTop: this.margin - (this.parentHeight * 2),
        viewportBottom: this.margin + this.parentHeight,
      });
    });

    it('calls the onPositionChange handler', () => {
      expect(this.props.onPositionChange).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.inside,
          previousPosition: undefined,
          event: null,
          waypointTop: this.margin + this.topSpacerHeight,
          waypointBottom: this.margin + this.topSpacerHeight,
          viewportTop: this.margin - (this.parentHeight * 2),
          viewportBottom: this.margin + this.parentHeight,
        });
    });

    it('does not call the onLeave handler', () => {
      expect(this.props.onLeave).not.toHaveBeenCalled();
    });
  });

  describe('when the Waypoint is visible on mount and has bottomOffset < -100%', () => {
    beforeEach(() => {
      this.props.bottomOffset = '-200%';

      this.topSpacerHeight = 90;
      this.bottomSpacerHeight = 200;
      this.parentComponent = this.subject();
      this.scrollable = this.parentComponent;
    });

    it('calls the onEnter handler', () => {
      expect(this.props.onEnter).toHaveBeenCalledWith({
        currentPosition: Waypoint.inside,
        previousPosition: undefined,
        event: null,
        waypointTop: this.margin + this.topSpacerHeight,
        waypointBottom: this.margin + this.topSpacerHeight,
        viewportTop: this.margin,
        viewportBottom: this.margin + (this.parentHeight * 3),
      });
    });

    it('calls the onPositionChange handler', () => {
      expect(this.props.onPositionChange).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.inside,
          previousPosition: undefined,
          event: null,
          waypointTop: this.margin + this.topSpacerHeight,
          waypointBottom: this.margin + this.topSpacerHeight,
          viewportTop: this.margin,
          viewportBottom: this.margin + (this.parentHeight * 3),
        });
    });

    it('does not call the onLeave handler', () => {
      expect(this.props.onLeave).not.toHaveBeenCalled();
    });
  });

  describe('when the Waypoint is visible on mount and offsets < -100%', () => {
    beforeEach(() => {
      this.props.topOffset = '-200%';
      this.props.bottomOffset = '-200%';

      this.topSpacerHeight = 90;
      this.bottomSpacerHeight = 200;
      this.parentComponent = this.subject();
      this.scrollable = this.parentComponent;
    });

    it('calls the onEnter handler', () => {
      expect(this.props.onEnter).toHaveBeenCalledWith({
        currentPosition: Waypoint.inside,
        previousPosition: undefined,
        event: null,
        waypointTop: this.margin + this.topSpacerHeight,
        waypointBottom: this.margin + this.topSpacerHeight,
        viewportTop: this.margin - (this.parentHeight * 2),
        viewportBottom: this.margin + (this.parentHeight * 3),
      });
    });

    it('calls the onPositionChange handler', () => {
      expect(this.props.onPositionChange).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.inside,
          previousPosition: undefined,
          event: null,
          waypointTop: this.margin + this.topSpacerHeight,
          waypointBottom: this.margin + this.topSpacerHeight,
          viewportTop: this.margin - (this.parentHeight * 2),
          viewportBottom: this.margin + (this.parentHeight * 3),
        });
    });

    it('does not call the onLeave handler', () => {
      expect(this.props.onLeave).not.toHaveBeenCalled();
    });
  });

  describe('when scrolling while the waypoint is visible', () => {
    beforeEach(() => {
      this.topSpacerHeight = 90;
      this.bottomSpacerHeight = 200;
      this.parentComponent = this.subject();
      this.scrollable = this.parentComponent;
      scrollNodeTo(this.scrollable, this.topSpacerHeight / 2);
    });

    it('does not call the onEnter handler again', () => {
      expect(this.props.onEnter.calls.count()).toBe(1);
    });

    it('does not call the onLeave handler', () => {
      expect(this.props.onLeave).not.toHaveBeenCalled();
    });

    it('does not call the onPositionChange handler again', () => {
      expect(this.props.onPositionChange.calls.count()).toBe(1);
    });
  });

  describe('when scrolling past the waypoint while it is visible', () => {
    beforeEach(() => {
      this.topSpacerHeight = 90;
      this.bottomSpacerHeight = 200;
      this.parentComponent = this.subject();
      this.scrollable = this.parentComponent;
      scrollNodeTo(this.scrollable, this.topSpacerHeight + 10);
    });

    it('the onLeave handler is called', () => {
      expect(this.props.onLeave).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.above,
          previousPosition: Waypoint.inside,
          event: jasmine.any(Event),
          waypointTop: this.margin - 10,
          waypointBottom: this.margin - 10,
          viewportTop: this.margin,
          viewportBottom: this.margin + this.parentHeight,
        });
    });

    it('does not call the onEnter handler', () => {
      expect(this.props.onEnter.calls.count()).toBe(1);
    });

    it('the onPositionChange is called', () => {
      expect(this.props.onPositionChange).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.above,
          previousPosition: Waypoint.inside,
          event: jasmine.any(Event),
          waypointTop: this.margin - 10,
          waypointBottom: this.margin - 10,
          viewportTop: this.margin,
          viewportBottom: this.margin + this.parentHeight,
        });
    });
  });

  describe('when the Waypoint is below the bottom', () => {
    beforeEach(() => {
      this.topSpacerHeight = 200;

      // The bottom spacer needs to be tall enough to force the Waypoint to exit
      // the viewport when scrolled all the way down.
      this.bottomSpacerHeight = 3000;
    });

    it('does not call the onEnter handler on mount', () => {
      this.subject();
      expect(this.props.onEnter).not.toHaveBeenCalled();
    });

    it('does not call the onLeave handler on mount', () => {
      this.subject();
      expect(this.props.onLeave).not.toHaveBeenCalled();
    });

    it('calls the onPositionChange handler', () => {
      this.subject();
      expect(this.props.onPositionChange).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.below,
          previousPosition: undefined,
          event: null,
          waypointTop: this.margin + this.topSpacerHeight,
          waypointBottom: this.margin + this.topSpacerHeight,
          viewportTop: this.margin,
          viewportBottom: this.margin + this.parentHeight,
        });
    });

    describe('with children', () => {
      beforeEach(() => {
        this.childrenHeight = 80;
        this.props.children = (
          <div>
            <div style={{ height: this.childrenHeight / 2 }} />
            <div style={{ height: this.childrenHeight / 2 }} />
          </div>
        );
      });

      it('calls the onEnter handler when scrolling down far enough', () => {
        this.component = this.subject();
        this.props.onPositionChange.calls.reset();
        scrollNodeTo(this.component, 100);

        expect(this.props.onEnter).
          toHaveBeenCalledWith({
            currentPosition: Waypoint.inside,
            previousPosition: Waypoint.below,
            event: jasmine.any(Event),
            waypointTop: this.margin + this.topSpacerHeight - 100,
            waypointBottom: this.margin + this.topSpacerHeight - 100 + this.childrenHeight,
            viewportTop: this.margin,
            viewportBottom: this.margin + this.parentHeight,
          });
      });
    });

    describe('when scrolling down just below the threshold', () => {
      beforeEach(() => {
        this.component = this.subject();
        this.props.onPositionChange.calls.reset();
        scrollNodeTo(this.component, 99);
      });

      it('does not call the onEnter handler', () => {
        expect(this.props.onEnter).not.toHaveBeenCalled();
      });

      it('does not call the onLeave handler', () => {
        expect(this.props.onLeave).not.toHaveBeenCalled();
      });

      it('does not call the onPositionChange handler', () => {
        expect(this.props.onPositionChange).not.toHaveBeenCalled();
      });
    });

    it('calls the onEnter handler when scrolling down past the threshold', () => {
      scrollNodeTo(this.subject(), 100);

      expect(this.props.onEnter).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.inside,
          previousPosition: Waypoint.below,
          event: jasmine.any(Event),
          waypointTop: this.margin + this.topSpacerHeight - 100,
          waypointBottom: this.margin + this.topSpacerHeight - 100,
          viewportTop: this.margin,
          viewportBottom: this.margin + this.parentHeight,
        });
    });

    it('calls the onPositionChange handler when scrolling down past the threshold', () => {
      scrollNodeTo(this.subject(), 100);

      expect(this.props.onPositionChange).
        toHaveBeenCalledWith({
          currentPosition: Waypoint.inside,
          previousPosition: Waypoint.below,
          event: jasmine.any(Event),
          waypointTop: this.margin + this.topSpacerHeight - 100,
          waypointBottom: this.margin + this.topSpacerHeight - 100,
          viewportTop: this.margin,
          viewportBottom: this.margin + this.parentHeight,
        });
    });

    it('does not call the onLeave handler when scrolling down past the threshold', () => {
      scrollNodeTo(this.subject(), 100);
      expect(this.props.onLeave).not.toHaveBeenCalled();
    });

    describe('when `fireOnRapidScroll` is disabled', () => {
      beforeEach(() => {
        this.props.fireOnRapidScroll = false;
      });

      it('calls the onEnter handler when scrolling down past the threshold', () => {
        scrollNodeTo(this.subject(), 100);

        expect(this.props.onEnter).
          toHaveBeenCalledWith({
            currentPosition: Waypoint.inside,
            previousPosition: Waypoint.below,
            event: jasmine.any(Event),
            waypointTop: this.margin + this.topSpacerHeight - 100,
            waypointBottom: this.margin + this.topSpacerHeight - 100,
            viewportTop: this.margin,
            viewportBottom: this.margin + this.parentHeight,
          });
      });

      it('calls the onPositionChange handler when scrolling down past the threshold', () => {
        scrollNodeTo(this.subject(), 100);

        expect(this.props.onPositionChange).
          toHaveBeenCalledWith({
            currentPosition: Waypoint.inside,
            previousPosition: Waypoint.below,
            event: jasmine.any(Event),
            waypointTop: this.margin + this.topSpacerHeight - 100,
            waypointBottom: this.margin + this.topSpacerHeight - 100,
            viewportTop: this.margin,
            viewportBottom: this.margin + this.parentHeight,
          });
      });

      it('does not call the onLeave handler when scrolling down past the threshold', () => {
        scrollNodeTo(this.subject(), 100);

        expect(this.props.onLeave).not.toHaveBeenCalled();
      });
    });

    describe('when scrolling quickly past the waypoint', () => {
      // If you scroll really fast, we might not get a scroll event when the
      // waypoint is in view. We will get a scroll event before going into view
      // though, and one after. We want to treat this as if the waypoint was
      // visible for a brief moment, and so we fire both onEnter and onLeave.
      beforeEach(() => {
        this.scrollQuicklyPast = () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, this.topSpacerHeight + this.bottomSpacerHeight);
        };
      });

      it('calls the onEnter handler', () => {
        this.scrollQuicklyPast();
        expect(this.props.onEnter).
          toHaveBeenCalledWith({
            currentPosition: Waypoint.inside,
            previousPosition: Waypoint.below,
            event: jasmine.any(Event),
            waypointTop: this.margin - this.bottomSpacerHeight + this.parentHeight,
            waypointBottom: this.margin - this.bottomSpacerHeight + this.parentHeight,
            viewportTop: this.margin,
            viewportBottom: this.margin + this.parentHeight,
          });
      });

      it('calls the onLeave handler', () => {
        this.scrollQuicklyPast();
        expect(this.props.onLeave).
          toHaveBeenCalledWith({
            currentPosition: Waypoint.above,
            previousPosition: Waypoint.inside,
            event: jasmine.any(Event),
            waypointTop: this.margin - this.bottomSpacerHeight + this.parentHeight,
            waypointBottom: this.margin - this.bottomSpacerHeight + this.parentHeight,
            viewportTop: this.margin,
            viewportBottom: this.margin + this.parentHeight,
          });
      });

      it('calls the onPositionChange handler', () => {
        this.scrollQuicklyPast();
        expect(this.props.onPositionChange).
          toHaveBeenCalledWith({
            currentPosition: Waypoint.above,
            previousPosition: Waypoint.below,
            event: jasmine.any(Event),
            waypointTop: this.margin - this.bottomSpacerHeight + this.parentHeight,
            waypointBottom: this.margin - this.bottomSpacerHeight + this.parentHeight,
            viewportTop: this.margin,
            viewportBottom: this.margin + this.parentHeight,
          });
      });

      describe('when `fireOnRapidScroll` is disabled', () => {
        beforeEach(() => {
          this.props.fireOnRapidScroll = false;
        });

        it('does not call the onEnter handler', () => {
          this.scrollQuicklyPast();
          expect(this.props.onEnter).not.toHaveBeenCalled();
        });

        it('does not call the onLeave handler', () => {
          this.scrollQuicklyPast();
          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('calls the onPositionChange handler', () => {
          this.scrollQuicklyPast();
          expect(this.props.onPositionChange).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.above,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin - this.bottomSpacerHeight + this.parentHeight,
              waypointBottom: this.margin - this.bottomSpacerHeight + this.parentHeight,
              viewportTop: this.margin,
              viewportBottom: this.margin + this.parentHeight,
            });
        });
      });
    });

    describe('with a non-zero topOffset', () => {
      describe('and the topOffset is passed as a percentage', () => {
        beforeEach(() => {
          this.props.topOffset = '-10%';
        });

        it('calls the onLeave handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 211);

          expect(this.props.onLeave).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.above,
              previousPosition: Waypoint.inside,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 211,
              waypointBottom: this.margin + this.topSpacerHeight - 211,
              viewportTop: this.margin + this.parentHeight * -0.1,
              viewportBottom: this.margin + this.parentHeight,
            });
        });
      });
    });

    describe('with a non-zero bottomOffset', () => {
      describe('and the bottomOffset is passed as a percentage', () => {
        beforeEach(() => {
          this.props.bottomOffset = '-10%';
        });

        it('does not call the onEnter handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onEnter).not.toHaveBeenCalled();
        });

        it('does not call the onLeave handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('does not call onPositionChange handler when scrolling down near bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onPositionChange).not.toHaveBeenCalled();
        });

        it('calls the onEnter handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onEnter).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + Math.floor(this.parentHeight * 1.1),
            });
        });

        it('does not call the onLeave handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('calls the onPositionChange handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onPositionChange).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + Math.floor(this.parentHeight * 1.1),
            });
        });
      });

      describe('and the bottom offset is passed as a numeric string', () => {
        beforeEach(() => {
          this.props.bottomOffset = '-10';
        });

        it('does not call the onEnter handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onEnter).not.toHaveBeenCalled();
        });

        it('does not call the onLeave handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('does not call onPositionChange handler when scrolling down near bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onPositionChange).not.toHaveBeenCalled();
        });

        it('calls the onEnter handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onEnter).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + this.parentHeight + 10,
            });
        });

        it('does not call the onLeave handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('calls the onPositionChange handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onPositionChange).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + this.parentHeight + 10,
            });
        });
      });

      describe('and the bottom offset is passed as a pixel string', () => {
        beforeEach(() => {
          this.props.bottomOffset = '-10px';
        });

        it('does not call the onEnter handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onEnter).not.toHaveBeenCalled();
        });

        it('does not call the onLeave handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('does not call onPositionChange handler when scrolling down near bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onPositionChange).not.toHaveBeenCalled();
        });

        it('calls the onEnter handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onEnter).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + this.parentHeight + 10,
            });
        });

        it('does not call the onLeave handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('calls the onPositionChange handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onPositionChange).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + this.parentHeight + 10,
            });
        });
      });

      describe('and the bottom offset is passed as a number', () => {
        beforeEach(() => {
          this.props.bottomOffset = -10;
        });

        it('does not call the onEnter handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onEnter).not.toHaveBeenCalled();
        });

        it('does not call the onLeave handler when scrolling down near the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('does not call onPositionChange handler when scrolling down near bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 89);

          expect(this.props.onPositionChange).not.toHaveBeenCalled();
        });

        it('calls the onEnter handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onEnter).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + this.parentHeight + 10,
            });
        });

        it('does not call the onLeave handler when scrolling down past the bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onLeave).not.toHaveBeenCalled();
        });

        it('calls onPositionChange handler when scrolling down past bottom offset', () => {
          this.component = this.subject();
          this.props.onPositionChange.calls.reset();
          scrollNodeTo(this.component, 90);

          expect(this.props.onPositionChange).
            toHaveBeenCalledWith({
              currentPosition: Waypoint.inside,
              previousPosition: Waypoint.below,
              event: jasmine.any(Event),
              waypointTop: this.margin + this.topSpacerHeight - 90,
              waypointBottom: this.margin + this.topSpacerHeight - 90,
              viewportTop: this.margin,
              viewportBottom: this.margin + this.parentHeight + 10,
            });
        });
      });
    });
  });

  describe('when the Waypoint has children', () => {
    it('does not throw with a DOM Element as a child', () => {
      this.props.children = <div />;
      expect(this.subject).not.toThrow();
    });

    it('errors when multiple children are provided', () => {
      // INFO: React 16 throws user errorrs again by it's own in dev mode, see:
      // https://github.com/facebook/react/blob/e932ad68bed656eed5295b61ba74e5d0857902ed/src/renderers/shared/fiber/ReactFiberErrorLogger.js#L65
      //
      // This code ignores uncaught errors and prevents test from failing in React 16
      window.onerror = () => undefined;

      this.props.children = [
        <div key={1} />,
        <div key={2} />,
        <div key={3} />,
      ];

      expect(this.subject).toThrowError(notValidErrorMessage);

      window.onerror = undefined;
    });

    it('does not throw with a Stateful Component as a child', () => {
      class StatefulComponent extends React.Component {
        render() {
          return <div ref={this.props.innerRef} />;
        }
      }

      this.props.children = <StatefulComponent />;
      expect(this.subject).not.toThrow();
    });

{'fraction_non_alphanumeric': 0.121900195455656, 'fraction_numerical': 0.00916198387490838, 'mean_word_length': 2.2736315921019745, 'pattern_counts': {'":': 0, '<': 42, '<?xml version=': 0, '>': 253, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '200908', 'n_tokens_mistral': 18369, 'n_tokens_neox': 17580, 'n_words': 3783}