Home Reference Source Repository

src/legacy/store/__tests__/GraphQLRange-test.js

/**
 * Copyright (c) 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @emails oncall+relay
 */

'use strict';

const RelayTestUtils = require('RelayTestUtils');

jest
  .unmock('GraphQLSegment')
  .unmock('GraphQLRange')
  .mock('warning');

const GraphQLRange = require('GraphQLRange');
const RelayConnectionInterface = require('RelayConnectionInterface');
const RelayRecord = require('RelayRecord');

function getFirstSegment(range) {
  return range.__debug().orderedSegments[0];
}

function getLastSegment(range) {
  const orderedSegments = range.__debug().orderedSegments;
  return orderedSegments[orderedSegments.length - 1];
}

function mockEdge(id, hasNullCursor) {
  const dataID = 'edge' + (hasNullCursor ? 'WithNullCursor' : '') + id;
  const edge = {
    __dataID__: dataID,
    node: {__dataID__: 'id' + id},
    cursor: (hasNullCursor ? null : 'cursor' + id),
  };
  return edge;
}

const edgeNeg10 = mockEdge('-10');
const edgeNeg9 = mockEdge('-9');
const edgeNeg3 = mockEdge('-3');
const edgeNeg2 = mockEdge('-2');
const edgeNeg1 = mockEdge('-1');
const edge0 = mockEdge('0');
const edge1 = mockEdge('1');
const edge2 = mockEdge('2');
const edge3 = mockEdge('3');
const edge4 = mockEdge('4');
const edge5 = mockEdge('5');
const edge96 = mockEdge('96');
const edge97 = mockEdge('97');
const edge98 = mockEdge('98');
const edge99 = mockEdge('99');
const edge100 = mockEdge('100');
const edge101 = mockEdge('101');
const edge102 = mockEdge('102');
const edge103 = mockEdge('103');
const edge104 = mockEdge('104');
const edge110 = mockEdge('110');
const edge111 = mockEdge('111');
const edgeWithNullCursor1 = mockEdge('1', true);
const edgeWithNullCursor2 = mockEdge('2', true);
const edgeWithNullCursor3 = mockEdge('3', true);

const first3Edges = [edge1, edge2, edge3];
const first5Edges = [edge1, edge2, edge3, edge4, edge5];
const last3Edges = [edge98, edge99, edge100];
const last5Edges = [edge96, edge97, edge98, edge99, edge100];

describe('GraphQLRange', () => {
  let consoleError;
  let consoleWarn;
  let range;

  let HAS_NEXT_PAGE, HAS_PREV_PAGE;

  beforeEach(() => {
    jest.resetModuleRegistry();
    consoleError = console.error;
    consoleWarn = console.warn;

    RelayRecord.getDataIDForObject.mockImplementation(function(data) {
      return data.__dataID__;
    });
    range = new GraphQLRange();

    ({HAS_NEXT_PAGE, HAS_PREV_PAGE} = RelayConnectionInterface);

    jasmine.addMatchers(RelayTestUtils.matchers);
  });

  afterEach(() => {
    console.error = consoleError;
    console.warn = consoleWarn;
  });

  it('should add for first() query', () => {
    const queryCalls = [
      {name: 'first', value: 3},
    ];
    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);

    // Request the full set
    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
  });

  it('should add for after().first() query', () => {
    let queryCalls = [
      {name: 'after', value: null},
      {name: 'first', value: 3},
    ];
    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(
      queryCalls,
      first3Edges,
      pageInfo
    );

    const incrementalEdges = [edge4, edge5];
    const incrementalPageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: true,
    };

    queryCalls = [
      {name: 'after', value: 'cursor3'},
      {name: 'first', value: 2},
    ];
    range.addItems(
      queryCalls,
      incrementalEdges,
      incrementalPageInfo
    );

    // Request the full set
    queryCalls = [
      {name: 'after', value: null},
      {name: 'first', value: 5},
    ];
    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual([
      edge1.__dataID__,
      edge2.__dataID__,
      edge3.__dataID__,
      edge4.__dataID__,
      edge5.__dataID__,
    ]);
    expect(result.diffCalls.length).toBe(0);

  });

  it('should add for after().first() query in last segment', () => {
    let queryCalls = [
      {name: 'last', value: 3},
    ];
    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    range.addItems(queryCalls, last3Edges, pageInfo);

    const incrementalQueryCall = [
      {name: 'after', value: 'cursor100'},
      {name: 'first', value: 2},
    ];
    const incrementalEdges = [edge101, edge102];
    const incrementalPageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    range.addItems(
      incrementalQueryCall,
      incrementalEdges,
      incrementalPageInfo
    );

    // Request the full set
    queryCalls = [
      {name: 'last', value: 5},
    ];
    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual([
      edge98.__dataID__,
      edge99.__dataID__,
      edge100.__dataID__,
      edge101.__dataID__,
      edge102.__dataID__,
    ]);
    expect(result.diffCalls.length).toBe(0);

  });

  it('should add for before().first() query', () => {
    let queryCalls = [
      {name: 'last', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);

    const incrementalQueryCall = [
      {name: 'before', value: 'cursor1'},
      {name: 'first', value: 2},
    ];

    const incrementalEdges = [edgeNeg1, edge0];
    const incrementalPageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: false,
    };
    range.addItems(
      incrementalQueryCall,
      incrementalEdges,
      incrementalPageInfo
    );
    // Request the full set to make sure it is stitched properly
    queryCalls = [
      {name: 'last', value: 5},
    ];
    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual([
      edgeNeg1.__dataID__,
      edge0.__dataID__,
      edge1.__dataID__,
      edge2.__dataID__,
      edge3.__dataID__,
    ]);
    expect(result.diffCalls.length).toBe(0);

  });

  it('should add for before().first() query with gap', () => {
    let queryCalls = [
      {name: 'first', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);

    const incrementalQueryCall = [
      {name: 'before', value: 'cursor1'},
      {name: 'first', value: 2},
    ];

    const incrementalEdges = [edgeNeg10, edgeNeg9];
    const incrementalPageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };
    range.addItems(
      incrementalQueryCall,
      incrementalEdges,
      incrementalPageInfo
    );
    // Request super set
    queryCalls = [
      {name: 'first', value: 5},
    ];
    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edgeNeg10.__dataID__, edgeNeg9.__dataID__]
    );
    expect(result.diffCalls).toEqual([
      {name: 'after', value: 'cursor-9'},
      {name: 'before', value: 'cursor1'},
      {name: 'first', value: 3},
    ]);

  });

  it('should not make empty segment for before().first() query with gap', () => {
    let queryCalls = [
      {name: 'first', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);

    const incrementalQueryCall = [
      {name: 'before', value: 'cursor1'},
      {name: 'first', value: 2},
    ];

    const incrementalEdges = [];
    const incrementalPageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };
    range.addItems(
      incrementalQueryCall,
      incrementalEdges,
      incrementalPageInfo
    );
    // Request super set
    queryCalls = [
      {name: 'first', value: 5},
    ];
    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls).toEqual([
      {name: 'after', value: 'cursor3'},
      {name: 'first', value: 2},
    ]);

  });

  it('should add for last() query', () => {
    const queryCalls = [
      {name: 'last', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    range.addItems(queryCalls, last3Edges, pageInfo);

    // Request the full set
    const result = range.retrieveRangeInfoForQuery(queryCalls, {count: 3});

    expect(result.requestedEdgeIDs).toEqual(
      [edge98.__dataID__, edge99.__dataID__, edge100.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
  });

  it('should add for before().last() query', () => {
    let queryCalls = [
      {name: 'last', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    range.addItems(queryCalls, last3Edges, pageInfo);

    const incrementalQueryCall = [
      {name: 'before', value: 'cursor98'},
      {name: 'last', value: 2},
    ];

    const incrementalEdges = [edge96, edge97];

    const incrementalPageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: true,
    };

    range.addItems(
      incrementalQueryCall,
      incrementalEdges,
      incrementalPageInfo
    );

    // Request the full set
    queryCalls = [
      {name: 'last', value: 5},
    ];
    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual([
      edge96.__dataID__,
      edge97.__dataID__,
      edge98.__dataID__,
      edge99.__dataID__,
      edge100.__dataID__,
    ]);
    expect(result.diffCalls.length).toBe(0);
  });

  it('should add for before().last() query in first segment', () => {
    let queryCalls = [
      {name: 'first', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);

    const incrementalQueryCall = [
      {name: 'before', value: 'cursor1'},
      {name: 'last', value: 2},
    ];

    const incrementalEdges = [edgeNeg1, edge0];
    const incrementalPageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(
      incrementalQueryCall,
      incrementalEdges,
      incrementalPageInfo
    );

    // Request the full set
    queryCalls = [
      {name: 'first', value: 5},
    ];
    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual([
      edgeNeg1.__dataID__,
      edge0.__dataID__,
      edge1.__dataID__,
      edge2.__dataID__,
      edge3.__dataID__,
    ]);
    expect(result.diffCalls.length).toBe(0);
  });

  it('should add for after().last() query', () => {
    let queryCalls = [
      {name: 'last', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    range.addItems(queryCalls, last3Edges, pageInfo);
    const incrementalQueryCall = [
      {name: 'after', value: 'cursor100'},
      {name: 'last', value: 2},
    ];

    const incrementalEdges = [edge101, edge102];
    const incrementalPageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(
      incrementalQueryCall,
      incrementalEdges,
      incrementalPageInfo
    );

    // Request the full set
    queryCalls = [
      {name: 'last', value: 5},
    ];
    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual([
      edge98.__dataID__,
      edge99.__dataID__,
      edge100.__dataID__,
      edge101.__dataID__,
      edge102.__dataID__,
    ]);
    expect(result.diffCalls.length).toBe(0);

  });

  it('should add for after().last() with gap', () => {
    let queryCalls = [
      {name: 'after', value: null},
      {name: 'last', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    range.addItems(queryCalls, last3Edges, pageInfo);

    queryCalls = [
      {name: 'after', value: 'cursor100'},
      {name: 'last', value: 2},
    ];
    range.addItems(
      queryCalls,
      [edge110, edge111],
      {[HAS_NEXT_PAGE]: false, [HAS_PREV_PAGE]: true}
    );

    // Request the super set
    queryCalls = [
      {name: 'after', value: null},
      {name: 'last', value: 5},
    ];
    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edge110.__dataID__, edge111.__dataID__]
    );
    expect(result.diffCalls).toEqual([
      {name: 'before', value: 'cursor110'},
      {name: 'after', value: 'cursor100'},
      {name: 'last', value: 3},
    ]);
  });

  it('should not make empty segment for after().last() query with gap', () => {
    let queryCalls = [
      {name: 'after', value: null},
      {name: 'last', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    range.addItems(queryCalls, last3Edges, pageInfo);

    queryCalls = [
      {name: 'after', value: 'cursor100'},
      {name: 'last', value: 2},
    ];
    range.addItems(
      queryCalls,
      [],
      {[HAS_NEXT_PAGE]: false, [HAS_PREV_PAGE]: true}
    );

    // Request the super set
    queryCalls = [
      {name: 'after', value: null},
      {name: 'last', value: 5},
    ];
    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edge98.__dataID__, edge99.__dataID__, edge100.__dataID__]
    );
    expect(result.diffCalls).toEqual([
      {name: 'before', value: 'cursor98'},
      {name: 'last', value: 2},
    ]);
  });

  it('should error for invalid call value', () => {
    console.error = jest.fn();
    const queryCalls = [
      {name: 'first', value: 0},
    ];

    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(console.error.mock.calls.length).toBe(1);
    expect(console.error.mock.calls[0]).toEqual([
      'GraphQLRange only supports first(<count>) or last(<count>) ' +
      'where count is greater than 0',
    ]);
    expect(result.requestedEdgeIDs).toEqual([]);
    expect(result.diffCalls.length).toBe(0);
  });

  it('should error for first().last() query', () => {
    console.error = jest.fn();
    const queryCalls = [
      {name: 'first', value: 3},
      {name: 'last', value: 3},
    ];

    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(console.error.mock.calls.length).toBe(1);
    expect(console.error.mock.calls[0]).toEqual([
      'GraphQLRange currently only handles first(<count>), ' +
      'after(<cursor>).first(<count>), last(<count>), ' +
      'before(<cursor>).last(<count>), before(<cursor>).first(<count>), ' +
      'and after(<cursor>).last(<count>)',
    ]);
    expect(result.requestedEdgeIDs).toEqual([]);
    expect(result.diffCalls.length).toBe(0);
  });

  it('should retrieve for first() queries', () => {
    let queryCalls = [
      {name: 'first', value: 3},
    ];

    // Request from empty range
    let result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual([]);
    expect(result.diffCalls).toEqual([{name: 'first', value: 3}]);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(false);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(true);

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);

    // Request the full set
    result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(false);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(true);

    // Request a subset
    queryCalls = [
      {name: 'first', value: 2},
    ];
    result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(false);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(true);

    // Request a superset
    queryCalls = [
      {name: 'first', value: 5},
    ];
    result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls).toEqual([
      {name: 'after', value: 'cursor3'},
      {name: 'first', value: 2},
    ]);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(false);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(true);

  });

  it('should retrieve for after().first() queries', () => {
    let queryCalls = [
      {name: 'after', value: null},
      {name: 'first', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(
      queryCalls,
      first3Edges,
      pageInfo
    );

    // Request a subset with after
    queryCalls = [
      {name: 'after', value: 'cursor1'},
      {name: 'first', value: 2},
    ];
    let result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edge2.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(false);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(true);

    // Request a superset with after
    queryCalls = [
      {name: 'after', value: 'cursor1'},
      {name: 'first', value: 5},
    ];
    result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edge2.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls).toEqual([
      {name: 'after', value: 'cursor3'},
      {name: 'first', value: 3},
    ]);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(false);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(true);

    // Request a non-intersecting superset with after
    queryCalls = [
      {name: 'after', value: 'cursor3'},
      {name: 'first', value: 2},
    ];
    result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual([]);
    expect(result.diffCalls).toEqual([
      {name: 'after', value: 'cursor3'},
      {name: 'first', value: 2},
    ]);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(false);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(true);
  });

  it('should retrieve for last() queries', () => {
    let queryCalls = [
      {name: 'last', value: 3},
    ];

    // Request the from empty range
    let result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual([]);
    expect(result.diffCalls).toEqual([{name: 'last', value: 3}]);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(true);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(false);

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    range.addItems(queryCalls, last3Edges, pageInfo);

    // Request the full set
    result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edge98.__dataID__, edge99.__dataID__, edge100.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(true);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(false);

    // Request a subset
    queryCalls = [{name: 'last', value: 2}];
    result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edge99.__dataID__, edge100.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(true);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(false);

    // Requst a superset
    queryCalls = [{name: 'last', value: 5}];
    result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edge98.__dataID__, edge99.__dataID__, edge100.__dataID__]
    );
    expect(result.diffCalls).toEqual([
      {name: 'before', value: 'cursor98'},
      {name: 'last', value: 2},
    ]);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(true);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(false);
  });

  it('should retrieve for before().last() queries', () => {
    const queryCalls = [
      {name: 'last', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    range.addItems(queryCalls, last3Edges, pageInfo);

    // Request a subset with before
    let result = range.retrieveRangeInfoForQuery([
      {name: 'before', value: 'cursor100'},
      {name: 'last', value: 2},
    ]);

    expect(result.requestedEdgeIDs).toEqual(
      [edge98.__dataID__, edge99.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(true);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(false);

    // Request a superset with before
    result = range.retrieveRangeInfoForQuery([
      {name: 'before', value: 'cursor100'},
      {name: 'last', value: 5},
    ]);

    expect(result.requestedEdgeIDs).toEqual(
      [edge98.__dataID__, edge99.__dataID__]
    );
    expect(result.diffCalls).toEqual([
      {name: 'before', value: 'cursor98'},
      {name: 'last', value: 3},
    ]);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(true);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(false);

    // Request a non-intersecting superset with before
    result = range.retrieveRangeInfoForQuery([
      {name: 'before', value: 'cursor98'},
      {name: 'last', value: 2},
    ]);

    expect(result.requestedEdgeIDs).toEqual([]);
    expect(result.diffCalls).toEqual([
      {name: 'before', value: 'cursor98'},
      {name: 'last', value: 2},
    ]);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(true);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(false);

  });

  it('should retrieve for after().first() from last segment', () => {
    const queryCalls = [
      {name: 'last', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };
    range.addItems(queryCalls, last3Edges, pageInfo);

    // Request a subset with after
    let result = range.retrieveRangeInfoForQuery([
      {name: 'after', value: 'cursor98'},
      {name: 'first', value: 1},
    ]);

    expect(result.requestedEdgeIDs).toEqual([edge99.__dataID__]);
    expect(result.diffCalls.length).toBe(0);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(false);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(true);

    // Request a superset with after
    result = range.retrieveRangeInfoForQuery([
      {name: 'after', value: 'cursor98'},
      {name: 'first', value: 5},
    ]);

    expect(result.requestedEdgeIDs).toEqual(
      [edge99.__dataID__, edge100.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(false);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(false);

    // Request a non-intersecting superset with after
    result = range.retrieveRangeInfoForQuery([
      {name: 'after', value: 'cursor100'},
      {name: 'first', value: 2},
    ]);

    expect(result.requestedEdgeIDs).toEqual([]);
    expect(result.diffCalls.length).toBe(0);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(false);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(false);

  });

  it('should retrieve for before().last() from first segment', () => {
    const queryCalls = [
      {name: 'first', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);

    // Request a subset with before
    let result = range.retrieveRangeInfoForQuery([
      {name: 'before', value: 'cursor3'},
      {name: 'last', value: 1},
    ]);

    expect(result.requestedEdgeIDs).toEqual([edge2.__dataID__]);
    expect(result.diffCalls.length).toBe(0);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(true);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(false);

    // Request a superset with before
    result = range.retrieveRangeInfoForQuery([
      {name: 'before', value: 'cursor3'},
      {name: 'last', value: 5},
    ]);

    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__]
    );
    expect(result.diffCalls).toEqual([]);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(false);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(false);

    // Request a non-intersecting superset with before
    result = range.retrieveRangeInfoForQuery([
      {name: 'before', value: 'cursor1'},
      {name: 'last', value: 2},
    ]);

    expect(result.requestedEdgeIDs).toEqual([]);
    expect(result.diffCalls).toEqual([]);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(false);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(false);

  });

  it('should support calls with no arguments', () => {
    const queryCalls = [
      {name: 'first', value: 3},
      {name: 'dummy_call', value: null},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);

    // Request the full set
    const result = range.retrieveRangeInfoForQuery([
      {name: 'first', value: 3},
      {name: 'dummy_call', value: null},
    ]);

    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
  });

  it('should support nodes with null cursors', () => {
    const queryCalls = [
      {name: 'first', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    const first3EdgesWithNullCursors = [
      edgeWithNullCursor1,
      edgeWithNullCursor2,
      edgeWithNullCursor3,
    ];

    range.addItems(queryCalls, first3EdgesWithNullCursors, pageInfo);

    // Request the full set
    const result = range.retrieveRangeInfoForQuery([
      {name: 'first', value: 3},
    ]);

    expect(result.requestedEdgeIDs).toEqual([
      'edgeWithNullCursor1',
      'edgeWithNullCursor2',
      'edgeWithNullCursor3',
    ]);
    expect(result.diffCalls.length).toBe(0);
  });

  it('should support prepending edge to range', () => {
    // Prepend on new range
    range.prependEdge(edge2);
    let result = range.retrieveRangeInfoForQuery([
      {name: 'first', value: 1},
    ]);
    expect(result.requestedEdgeIDs).toEqual([edge2.__dataID__]);
    expect(result.diffCalls.length).toBe(0);

    // Prepend on range that already has edge
    range.prependEdge(edge1);
    result = range.retrieveRangeInfoForQuery([
      {name: 'first', value: 2},
    ]);
    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
  });

  it('should support appending edge to range', () => {
    // Append on new range
    range.appendEdge(edge1);
    let result = range.retrieveRangeInfoForQuery([
      {name: 'last', value: 1},
    ]);
    expect(result.requestedEdgeIDs).toEqual([edge1.__dataID__]);
    expect(result.diffCalls.length).toBe(0);

    // Append on range that already has an edge
    range.appendEdge(edge2);
    result = range.retrieveRangeInfoForQuery([
      {name: 'last', value: 2},
    ]);
    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
  });

  it('should support bumping', () => {
    let queryCalls = [
      {name: 'first', value: 3},
    ];

    let pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);

    const afterQueryCalls = [
      {name: 'after', value: 'cursor3'},
      {name: 'first', value: 1},
    ];

    // Testing add after: adding id2 to end of range
    range.addItems(afterQueryCalls, [first3Edges[1]], pageInfo);
    let result = range.retrieveRangeInfoForQuery(queryCalls);
    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge3.__dataID__, edge2.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);

    // Testing prepend: adding id3 to the front of the range
    range.prependEdge(first3Edges[2]);
    result = range.retrieveRangeInfoForQuery(queryCalls);
    expect(result.requestedEdgeIDs).toEqual(
      [edge3.__dataID__, edge1.__dataID__, edge2.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);

    queryCalls = [
      {name: 'last', value: 3},
    ];

    pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    range.addItems(queryCalls, last3Edges, pageInfo);

    const beforeQueryCalls = [
      {name: 'before', value: 'cursor98'},
      {name: 'last', value: 1},
    ];

    // Testing add before: adding id99 to end of range
    range.addItems(beforeQueryCalls, [last3Edges[1]], pageInfo);
    result = range.retrieveRangeInfoForQuery(queryCalls);
    expect(result.requestedEdgeIDs).toEqual(
      [edge99.__dataID__, edge98.__dataID__, edge100.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);

    // Testing append: adding id98 to the end of the range
    range.appendEdge(last3Edges[0]);
    result = range.retrieveRangeInfoForQuery(queryCalls);
    expect(result.requestedEdgeIDs).toEqual(
      [edge99.__dataID__, edge100.__dataID__, edge98.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
  });

  it('should not generate diff query when range is empty', () => {
    const queryFirstCalls = [
      {name: 'first', value: 3},
    ];

    const queryLastCalls = [
      {name: 'last', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: false,
    };

    // Add empty first edges
    range.addItems(queryFirstCalls, [], pageInfo);

    let result = range.retrieveRangeInfoForQuery(queryFirstCalls);
    expect(result.diffCalls.length).toBe(0);
    result = range.retrieveRangeInfoForQuery(queryLastCalls);
    expect(result.diffCalls.length).toBe(0);

    // Add empty last edges
    range = new GraphQLRange();
    range.addItems(queryLastCalls, [], pageInfo);

    result = range.retrieveRangeInfoForQuery(queryFirstCalls);
    expect(result.diffCalls.length).toBe(0);
    result = range.retrieveRangeInfoForQuery(queryLastCalls);
    expect(result.diffCalls.length).toBe(0);
  });

  it('should collesce segments when we reach end', () => {
    const queryFirstCalls = [
      {name: 'first', value: 1},
    ];

    const queryLastCalls = [
      {name: 'last', value: 1},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryFirstCalls, [edge1], pageInfo);
    range.addItems(queryLastCalls, [edge1], pageInfo);

    let result = range.retrieveRangeInfoForQuery(queryFirstCalls);
    expect(result.requestedEdgeIDs).toEqual([edge1.__dataID__]);
    expect(result.diffCalls.length).toBe(0);
    result = range.retrieveRangeInfoForQuery(queryLastCalls);
    expect(result.requestedEdgeIDs).toEqual([edge1.__dataID__]);
    expect(result.diffCalls.length).toBe(0);
  });

  it('should not generate diff query when there is no more', () => {
    let queryCalls = [
      {name: 'first', value: 3},
    ];

    let pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };
    const beforeQueryCalls = [
      {name: 'before', value: 'cursor1'},
      {name: 'last', value: 1},
    ];

    range.addItems(queryCalls, first3Edges, pageInfo);
    let result = range.retrieveRangeInfoForQuery(beforeQueryCalls);
    // We know there is no more before cursor1 since that is the first edge
    expect(result.diffCalls.length).toBe(0);

    queryCalls = [
      {name: 'last', value: 3},
    ];

    pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };
    const afterQueryCalls = [
      {name: 'after', value: 'cursor100'},
      {name: 'first', value: 1},
    ];

    range.addItems(queryCalls, last3Edges, pageInfo);
    result = range.retrieveRangeInfoForQuery(afterQueryCalls);
    // We know there is no more after cursor100 since that is the last edge
    expect(result.diffCalls.length).toBe(0);
  });

  it('should add  and retrieve for surrounds() query', () => {
    const queryCalls = [
      {name: 'surrounds', value: ['id2', 1]},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);
    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
  });

  it('should not return surrounds query data for first query', () => {
    const surroundQueryCalls = [
      {name: 'surrounds', value: ['id2', 1]},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(surroundQueryCalls, first3Edges, pageInfo);

    const firstQueryCalls = [
      {name: 'first', value: 5},
    ];

    const resultForFirstQuery = range.retrieveRangeInfoForQuery(
      firstQueryCalls,
    );

    expect(resultForFirstQuery.requestedEdgeIDs).toEqual([]);
    expect(resultForFirstQuery.diffCalls).toEqual(firstQueryCalls);
  });

  it('should warn when reconciling conflicting first() ranges', () => {
    console.error = jest.fn();

    const queryCalls = [
      {name: 'first', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, [edge1, edge2, edge3], pageInfo);
    range.addItems(queryCalls, [edge1, edge3, edge4], pageInfo);

    expect(console.error.mock.calls.length).toBe(0);
    expect([
      'Relay was unable to reconcile edges on a connection. This most ' +
      'likely occurred while trying to handle a server response that ' +
      'includes connection edges with nodes that lack an `id` field.',
    ]).toBeWarnedNTimes(1);

    const result = range.retrieveRangeInfoForQuery(queryCalls);
    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__, edge3.__dataID__]
    );
  });

  it('should warn when reconciling conflicting last() ranges', () => {
    console.error = jest.fn();

    const queryCalls = [
      {name: 'last', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    // Add items twice
    range.addItems(queryCalls, [edge98, edge99, edge100], pageInfo);
    range.addItems(queryCalls, [edge98, edge1, edge100], pageInfo);

    expect(console.error.mock.calls.length).toBe(0);
    expect([
      'Relay was unable to reconcile edges on a connection. This most ' +
      'likely occurred while trying to handle a server response that ' +
      'includes connection edges with nodes that lack an `id` field.',
    ]).toBeWarnedNTimes(1);

    const result = range.retrieveRangeInfoForQuery(queryCalls);
    expect(result.requestedEdgeIDs).toEqual(
      [edge98.__dataID__, edge99.__dataID__, edge100.__dataID__]
    );
  });

  it('should reconcile duplicated queries', () => {
    console.error = jest.fn();
    console.warn = jest.fn();

    let queryCalls = [
      {name: 'first', value: 3},
    ];

    let pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    // Add items twice
    range.addItems(queryCalls, first3Edges, pageInfo);
    range.addItems(queryCalls, first3Edges, pageInfo);

    expect(console.error.mock.calls.length).toBe(0);
    expect(console.warn.mock.calls.length).toBe(0);

    let result = range.retrieveRangeInfoForQuery(queryCalls);
    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__, edge3.__dataID__]
    );

    queryCalls = [
      {name: 'last', value: 3},
    ];

    pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    // Add items twice
    range.addItems(queryCalls, last3Edges, pageInfo);
    range.addItems(queryCalls, last3Edges, pageInfo);

    expect(console.error.mock.calls.length).toBe(0);
    expect(console.warn.mock.calls.length).toBe(0);

    result = range.retrieveRangeInfoForQuery(queryCalls);
    expect(result.requestedEdgeIDs).toEqual(
      [edge98.__dataID__, edge99.__dataID__, edge100.__dataID__]
    );
  });

  it('should reconcile duplicated queries with no cursor', () => {
    console.error = jest.fn();
    console.warn = jest.fn();

    let queryCalls = [
      {name: 'first', value: 3},
    ];

    let pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };
    const e1 = mockEdge('1', true);
    const e2 = mockEdge('2', true);
    const e3 = mockEdge('3', true);

    let edges = [e1, e2, e3];

    // Add items twice
    range.addItems(queryCalls, edges, pageInfo);
    range.addItems(queryCalls, edges, pageInfo);

    expect(console.error.mock.calls.length).toBe(0);
    expect(console.warn.mock.calls.length).toBe(0);

    let result = range.retrieveRangeInfoForQuery(queryCalls);
    expect(result.requestedEdgeIDs).toEqual(
      [e1.__dataID__, e2.__dataID__, e3.__dataID__]
    );

    queryCalls = [
      {name: 'last', value: 3},
    ];

    pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };
    const e100 = mockEdge('100', true);
    const e99 = mockEdge('99', true);
    const e98 = mockEdge('98', true);

    edges = [e98, e99, e100];

    // Add items twice
    range.addItems(queryCalls, edges, pageInfo);
    range.addItems(queryCalls, edges, pageInfo);

    expect(console.error.mock.calls.length).toBe(0);
    expect(console.warn.mock.calls.length).toBe(0);

    result = range.retrieveRangeInfoForQuery(queryCalls);
    expect(result.requestedEdgeIDs).toEqual(
      [e98.__dataID__, e99.__dataID__, e100.__dataID__]
    );
  });

  it('should reconcile extending queries', () => {
    console.error = jest.fn();
    console.warn = jest.fn();

    let queryCalls = [
      {name: 'first', value: 3},
    ];

    let pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);

    queryCalls = [
      {name: 'first', value: 5},
    ];
    range.addItems(queryCalls, first5Edges, pageInfo);

    expect(console.error.mock.calls.length).toBe(0);
    expect(console.warn.mock.calls.length).toBe(0);

    let result = range.retrieveRangeInfoForQuery(queryCalls);
    expect(result.requestedEdgeIDs).toEqual([
      edge1.__dataID__,
      edge2.__dataID__,
      edge3.__dataID__,
      edge4.__dataID__,
      edge5.__dataID__,
    ]);

    queryCalls = [
      {name: 'last', value: 3},
    ];

    pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    range.addItems(queryCalls, last3Edges, pageInfo);

    queryCalls = [
      {name: 'last', value: 5},
    ];
    range.addItems(queryCalls, last5Edges, pageInfo);

    expect(console.error.mock.calls.length).toBe(0);
    expect(console.warn.mock.calls.length).toBe(0);

    result = range.retrieveRangeInfoForQuery(queryCalls);
    expect(result.requestedEdgeIDs).toEqual([
      edge96.__dataID__,
      edge97.__dataID__,
      edge98.__dataID__,
      edge99.__dataID__,
      edge100.__dataID__,
    ]);
  });

  it('should stitch first and last segment', () => {
    const firstQueryCalls = [
      {name: 'first', value: 3},
    ];
    const lastQueryCalls = [
      {name: 'last', value: 3},
    ];

    let pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(firstQueryCalls, first3Edges, pageInfo);
    let result = range.retrieveRangeInfoForQuery(lastQueryCalls);

    expect(result.diffCalls).toEqual([
      {name: 'after', value: 'cursor3'},
      {name: 'last', value: 3},
    ]);

    pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: false,
    };
    range.addItems(result.diffCalls, last3Edges, pageInfo);
    result = range.retrieveRangeInfoForQuery(
      [{name: 'first', value: 6}],
    );
    expect(result.diffCalls.length).toBe(0);
    expect(result.requestedEdgeIDs).toEqual([
      edge1.__dataID__,
      edge2.__dataID__,
      edge3.__dataID__,
      edge98.__dataID__,
      edge99.__dataID__,
      edge100.__dataID__,
    ]);
    result = range.retrieveRangeInfoForQuery(
      [{name: 'last', value: 6}],
    );
    expect(result.diffCalls.length).toBe(0);
    expect(result.requestedEdgeIDs).toEqual([
      edge1.__dataID__,
      edge2.__dataID__,
      edge3.__dataID__,
      edge98.__dataID__,
      edge99.__dataID__,
      edge100.__dataID__,
    ]);

    range = new GraphQLRange();

    pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    range.addItems(lastQueryCalls, last3Edges, pageInfo);
    result = range.retrieveRangeInfoForQuery(firstQueryCalls);

    expect(result.diffCalls).toEqual([
      {name: 'before', value: 'cursor98'},
      {name: 'first', value: 3},
    ]);

    pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(result.diffCalls, first3Edges, pageInfo);
    result = range.retrieveRangeInfoForQuery(
      [{name: 'first', value: 6}],
    );
    expect(result.diffCalls.length).toBe(0);
    expect(result.requestedEdgeIDs).toEqual([
      edge1.__dataID__,
      edge2.__dataID__,
      edge3.__dataID__,
      edge98.__dataID__,
      edge99.__dataID__,
      edge100.__dataID__,
    ]);
    result = range.retrieveRangeInfoForQuery(
      [{name: 'last', value: 6}],
    );
    expect(result.diffCalls.length).toBe(0);
    expect(result.requestedEdgeIDs).toEqual([
      edge1.__dataID__,
      edge2.__dataID__,
      edge3.__dataID__,
      edge98.__dataID__,
      edge99.__dataID__,
      edge100.__dataID__,
    ]);
  });

  it('should stitch up gap in first segment', () => {
    // Add initial edges
    const queryCalls = [
      {name: 'first', value: 3},
    ];

    let pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);

    let result = range.retrieveRangeInfoForQuery(queryCalls);

    // Create gap
    const incrementalQueryCall = [
      {name: 'before', value: 'cursor1'},
      {name: 'first', value: 2},
    ];
    const incrementalEdges = [edgeNeg3, edgeNeg2];
    const incrementalPageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };
    range.addItems(
      incrementalQueryCall,
      incrementalEdges,
      incrementalPageInfo
    );

    result = range.retrieveRangeInfoForQuery([
      {name: 'first', value: 5},
    ]);
    const diffCalls = result.diffCalls;
    expect(result.diffCalls).toEqual([
      {name: 'after', value: 'cursor-2'},
      {name: 'before', value: 'cursor1'},
      {name: 'first', value: 3},
    ]);

    // Fill in gap
    const gapEdges = [edgeNeg1, edge0];
    pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: false,
    };
    range.addItems(diffCalls, gapEdges, pageInfo);

    result = range.retrieveRangeInfoForQuery([
      {name: 'first', value: 5},
    ]);
    expect(result.requestedEdgeIDs).toEqual([
      edgeNeg3.__dataID__,
      edgeNeg2.__dataID__,
      edgeNeg1.__dataID__,
      edge0.__dataID__,
      edge1.__dataID__,
    ]);
    expect(result.diffCalls.length).toBe(0);
  });

  it('should stitch up gap in last segment', () => {
    // Add initial edges
    const queryCalls = [
      {name: 'last', value: 3},
    ];
    let pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };
    range.addItems(queryCalls, last3Edges, pageInfo);

    let result = range.retrieveRangeInfoForQuery(queryCalls);

    // Create gap
    const incrementalQueryCall = [
      {name: 'after', value: 'cursor100'},
      {name: 'last', value: 2},
    ];

    const incrementalEdges = [edge103, edge104];
    const incrementalPageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };
    range.addItems(
      incrementalQueryCall,
      incrementalEdges,
      incrementalPageInfo
    );

    result = range.retrieveRangeInfoForQuery([
      {name: 'last', value: 5},
    ]);
    const diffCalls = result.diffCalls;
    expect(result.diffCalls).toEqual([
      {name: 'before', value: 'cursor103'},
      {name: 'after', value: 'cursor100'},
      {name: 'last', value: 3},
    ]);

    // Fill in gap
    const gapEdges = [edge101, edge102];
    pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: false,
    };
    range.addItems(diffCalls, gapEdges, pageInfo);

    result = range.retrieveRangeInfoForQuery([
      {name: 'last', value: 5},
    ]);
    expect(result.requestedEdgeIDs).toEqual([
      edge100.__dataID__,
      edge101.__dataID__,
      edge102.__dataID__,
      edge103.__dataID__,
      edge104.__dataID__,
    ]);
    expect(result.diffCalls.length).toBe(0);
  });

  it('should refetch for whole ranges for null cursor', () => {
    const queryCalls = [
      {name: 'first', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    const nullCursorEdges = [
      edgeWithNullCursor1,
      edgeWithNullCursor2,
      edgeWithNullCursor3,
    ];

    range.addItems(queryCalls, nullCursorEdges, pageInfo);
    const five = [{name: 'first', value: 5}];
    const result = range.retrieveRangeInfoForQuery(five);
    expect(result.requestedEdgeIDs).toEqual([
      'edgeWithNullCursor1',
      'edgeWithNullCursor2',
      'edgeWithNullCursor3',
    ]);
    expect(result.diffCalls).toEqual(five);
  });

  it('replaces whole first() ranges when working with null cursors', () => {
    const queryCalls = [
      {name: 'first', value: 1},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
    };

    const nullCursorEdges = [
      edgeWithNullCursor1,
      edgeWithNullCursor2,
      edgeWithNullCursor3,
    ];

    // we don't replace empty ranges
    let segment = getFirstSegment(range);
    range.addItems(queryCalls, nullCursorEdges.slice(0, 1), pageInfo);
    expect(segment).toBe(getFirstSegment(range));

    // if we request more results but get the same number, we replace
    // (in case there were deleted items, different items, or reordering)
    const three = [{name: 'first', value: 3}];
    range.addItems(three, nullCursorEdges.slice(0, 1), pageInfo);
    expect(segment).not.toBe(getFirstSegment(range));

    // if the range has gotten bigger, we replace it
    segment = getFirstSegment(range);
    range.addItems(three, nullCursorEdges, pageInfo);
    expect(segment).not.toBe(getFirstSegment(range));

    // if the range has gotten bigger but has cursor info, we don't replace it
    const cursorEdges = [
      edge0,
      edge1,
      edge2,
    ];
    range = new GraphQLRange();
    segment = getFirstSegment(range);
    range.addItems(queryCalls, cursorEdges.slice(0, 1), pageInfo);
    expect(segment).toBe(getFirstSegment(range));
    range.addItems(three, cursorEdges, pageInfo);
    expect(segment).toBe(getFirstSegment(range));
  });

  it('replaces whole last() ranges when working with null cursors', () => {
    const queryCalls = [
      {name: 'last', value: 1},
    ];

    const pageInfo = {
      [HAS_PREV_PAGE]: true,
    };

    const nullCursorEdges = [
      edgeWithNullCursor1,
      edgeWithNullCursor2,
      edgeWithNullCursor3,
    ];

    // we don't replace empty ranges
    let segment = getLastSegment(range);
    range.addItems(queryCalls, nullCursorEdges.slice(2), pageInfo);
    expect(segment).toBe(getLastSegment(range));

    // if we request more results but get the same number, we replace
    // (in case there were deleted items, different items, or reordering)
    const three = [{name: 'last', value: 3}];
    range.addItems(three, nullCursorEdges.slice(2), pageInfo);
    expect(segment).not.toBe(getLastSegment(range));

    // if the range has gotten bigger, we replace it
    segment = getLastSegment(range);
    range.addItems(three, nullCursorEdges, pageInfo);
    expect(segment).not.toBe(getLastSegment(range));

    // if the range has gotten bigger but has cursor info, we don't replace it
    const cursorEdges = [
      edge0,
      edge1,
      edge2,
    ];
    range = new GraphQLRange();
    segment = getLastSegment(range);
    range.addItems(queryCalls, cursorEdges.slice(2), pageInfo);
    expect(segment).toBe(getLastSegment(range));
    range.addItems(three, cursorEdges, pageInfo);
    expect(segment).toBe(getLastSegment(range));
  });

  it('should retrieve correct page_info for ranges with null cursors', () => {
    const two = [{name: 'first', value: 2}];
    const three = [{name: 'first', value: 3}];

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: false,
    };

    const nullCursorEdges = [
      edgeWithNullCursor1,
      edgeWithNullCursor2,
      edgeWithNullCursor3,
    ];

    range.addItems(three, nullCursorEdges, pageInfo);
    let result = range.retrieveRangeInfoForQuery(two);
    expect(result.requestedEdgeIDs).toEqual([
      'edgeWithNullCursor1',
      'edgeWithNullCursor2',
    ]);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(true);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(false);

    result = range.retrieveRangeInfoForQuery(three);
    expect(result.requestedEdgeIDs).toEqual([
      'edgeWithNullCursor1',
      'edgeWithNullCursor2',
      'edgeWithNullCursor3',
    ]);
    expect(result.pageInfo[HAS_NEXT_PAGE]).toBe(false);
    expect(result.pageInfo[HAS_PREV_PAGE]).toBe(false);
  });

  it('should delete', () => {
    const queryCalls = [
      {name: 'first', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);
    range.removeEdgeWithID(edge2.__dataID__);
    const result = range.retrieveRangeInfoForQuery(queryCalls);
    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls).toEqual([
      {name: 'after', value: 'cursor3'},
      {name: 'first', value: 1},
    ]);
  });

  it('should not retrieve deleted bumped edges', () => {
    const queryCalls = [
      {name: 'first', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);
    let result = range.retrieveRangeInfoForQuery(queryCalls);

    // bump the second edge
    const afterQueryCalls = [
      {name: 'after', value: 'cursor3'},
      {name: 'first', value: 1},
    ];

    range.addItems(afterQueryCalls, [first3Edges[1]], pageInfo);

    // delete the second edge
    range.removeEdgeWithID(edge2.__dataID__);
    result = range.retrieveRangeInfoForQuery(queryCalls);
    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls).toEqual([
      {name: 'after', value: 'cursor3'},
      {name: 'first', value: 1},
    ]);
  });

  it('should retrieve with deleted bumped edges cursor', () => {
    const queryCalls = [
      {name: 'first', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };
    range.addItems(queryCalls, first3Edges, pageInfo);
    let result = range.retrieveRangeInfoForQuery(queryCalls);

    // bump the second edge
    const afterQueryCalls = [
      {name: 'after', value: 'cursor3'},
      {name: 'first', value: 1},
    ];
    const bumpedEdge = {...edge2};
    bumpedEdge.cursor = 'differentCursor';
    range.addItems(afterQueryCalls, [bumpedEdge], pageInfo);

    const queryCallsWithCursor = [
      {name: 'after', value: 'cursor2'},
      {name: 'first', value: 3},
    ];
    result = range.retrieveRangeInfoForQuery(queryCallsWithCursor);
    expect(result.requestedEdgeIDs).toEqual(
      [edge3.__dataID__, bumpedEdge.__dataID__]
    );
    expect(result.diffCalls).toEqual([
      {name: 'after', value: 'differentCursor'},
      {name: 'first', value: 1},
    ]);
  });

  it('should retrieve info for first() query given optimistic data', () => {
    const queryCalls = [
      {name: 'first', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);

    let result = range.retrieveRangeInfoForQuery(
      [{name: 'first', value: 3}],
      {__rangeOperationPrepend__: [edge4.__dataID__]}
    );

    expect(result.requestedEdgeIDs).toEqual(
      [edge4.__dataID__, edge1.__dataID__, edge2.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);

    result = range.retrieveRangeInfoForQuery(
      [{name: 'first', value: 3}],
      {__rangeOperationPrepend__: [edge4.__dataID__, edge5.__dataID__]}
    );

    expect(result.requestedEdgeIDs).toEqual(
      [edge4.__dataID__, edge5.__dataID__, edge1.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);

    // append shouldn't affect 'first' call
    result = range.retrieveRangeInfoForQuery(
      [{name: 'first', value: 3}],
      {__rangeOperationAppend__: [edge4.__dataID__, edge5.__dataID__]}
    );

    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);

    result = range.retrieveRangeInfoForQuery(
      [{name: 'first', value: 2}],
      {
        __rangeOperationRemove__: [edge1.__dataID__],
      }
    );

    expect(result.requestedEdgeIDs).toEqual(
      [edge2.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);

    result = range.retrieveRangeInfoForQuery(
      [{name: 'first', value: 3}],
      {
        __rangeOperationPrepend__: [edge4.__dataID__, edge5.__dataID__],
        __rangeOperationRemove__: [edge4.__dataID__, edge1.__dataID__],
      }
    );

    expect(result.requestedEdgeIDs).toEqual(
      [edge5.__dataID__, edge2.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
  });

  it('should retrieve optimistically appended edges when the last edge has been fetched', () => {
    const queryCalls = [
      {name: 'first', value: 3},
    ];

    // No next page means we have the very last edge.
    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);

    let result = range.retrieveRangeInfoForQuery(
      [{name: 'first', value: 4}],
      {__rangeOperationAppend__: [edge4.__dataID__]}
    );

    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__, edge3.__dataID__, edge4.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);

    // Should not return extra edges
    result = range.retrieveRangeInfoForQuery(
      [{name: 'first', value: 3}],
      {__rangeOperationAppend__: [edge4.__dataID__]}
    );

    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
  });

  it('should retrieve info for last() query given optimistic data', () => {
    const queryCalls = [
      {name: 'last', value: 3},
    ];

    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: true,
    };

    range.addItems(queryCalls, last3Edges, pageInfo);

    let result = range.retrieveRangeInfoForQuery(
      [{name: 'last', value: 3}],
      {__rangeOperationAppend__: [edge97.__dataID__]}
    );

    expect(result.requestedEdgeIDs).toEqual(
      [edge99.__dataID__, edge100.__dataID__, edge97.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);

    result = range.retrieveRangeInfoForQuery(
      [{name: 'last', value: 3}],
      {__rangeOperationAppend__: [edge97.__dataID__, edge96.__dataID__]}
    );

    expect(result.requestedEdgeIDs).toEqual(
      [edge100.__dataID__, edge97.__dataID__, edge96.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);

    // prepend shouldn't affect 'last' call
    result = range.retrieveRangeInfoForQuery(
      [{name: 'last', value: 3}],
      {__rangeOperationPrepend__: [edge97.__dataID__, edge96.__dataID__]}
    );

    expect(result.requestedEdgeIDs).toEqual(
      [edge98.__dataID__, edge99.__dataID__, edge100.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);

    result = range.retrieveRangeInfoForQuery(
      [{name: 'last', value: 2}],
      {__rangeOperationRemove__: [edge99.__dataID__]}
    );

    expect(result.requestedEdgeIDs).toEqual(
      [edge98.__dataID__, edge100.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);

    result = range.retrieveRangeInfoForQuery(
      [{name: 'last', value: 3}],
      {
        __rangeOperationAppend__: [edge97.__dataID__, edge96.__dataID__],
        __rangeOperationRemove__: [edge100.__dataID__, edge96.__dataID__],
      }
    );

    expect(result.requestedEdgeIDs).toEqual(
      [edge98.__dataID__, edge99.__dataID__, edge97.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
  });

  it('should retrieve optimistically prepended edges when the first edge has been fetched', () => {
    const queryCalls = [
      {name: 'last', value: 3},
    ];

    // No previous page means we have the very first edge.
    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, last3Edges, pageInfo);

    let result = range.retrieveRangeInfoForQuery(
      [{name: 'last', value: 4}],
      {__rangeOperationPrepend__: [edge97.__dataID__]}
    );

    expect(result.requestedEdgeIDs).toEqual([
      edge97.__dataID__,
      edge98.__dataID__,
      edge99.__dataID__,
      edge100.__dataID__,
    ]);
    expect(result.diffCalls.length).toBe(0);

    // Should not return extra edges
    result = range.retrieveRangeInfoForQuery(
      [{name: 'last', value: 3}],
      {__rangeOperationPrepend__: [edge97.__dataID__]}
    );

    expect(result.requestedEdgeIDs).toEqual([
      edge98.__dataID__,
      edge99.__dataID__,
      edge100.__dataID__,
    ]);
    expect(result.diffCalls.length).toBe(0);
  });

  it('should toJSON', () => {
    const queryCalls = [
      {name: 'first', value: 3},
    ];
    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);
    const actual = JSON.stringify(range);

    expect(actual).toEqual('[true,false,{},[[{' +
      '"0":{"edgeID":"edge1","cursor":"cursor1","deleted":false},' +
      '"1":{"edgeID":"edge2","cursor":"cursor2","deleted":false},' +
      '"2":{"edgeID":"edge3","cursor":"cursor3","deleted":false}},' +
      '{"edge1":[0],"edge2":[1],"edge3":[2]},' +
      '{"cursor1":0,"cursor2":1,"cursor3":2},0,2,3],' +
      '[{},{},{},null,null,0]]]'
    );

    range = GraphQLRange.fromJSON(JSON.parse(actual));

    // Request the full set
    const result = range.retrieveRangeInfoForQuery(queryCalls);

    expect(result.requestedEdgeIDs).toEqual(
      [edge1.__dataID__, edge2.__dataID__, edge3.__dataID__]
    );
    expect(result.diffCalls.length).toBe(0);
  });

  it('returns the DataIDs of all edges', () => {
    // Add a static edges
    const surroundQueryCalls = [
      {name: 'surrounds', value: ['id2', 1]},
    ];
    const pageInfo = {
      [HAS_NEXT_PAGE]: false,
      [HAS_PREV_PAGE]: false,
    };
    range.addItems(surroundQueryCalls, first3Edges, pageInfo);

    // Non-static edges
    const queryCalls = [
      {name: 'last', value: 3},
    ];
    range.addItems(queryCalls, last3Edges, pageInfo);
    // Sorting the IDs to make testing easier.
    expect(range.getEdgeIDs().sort()).toEqual([
      edge1.__dataID__,
      edge100.__dataID__,
      edge2.__dataID__,
      edge3.__dataID__,
      edge98.__dataID__,
      edge99.__dataID__,
    ]);
  });

  it('returns correct segmented edge ids', () => {
    // Starts off with two empty segments.
    expect(range.getSegmentedEdgeIDs()).toEqual([[], []]);

    const queryCalls = [
      {name: 'first', value: 3},
    ];
    const pageInfo = {
      [HAS_NEXT_PAGE]: true,
      [HAS_PREV_PAGE]: false,
    };

    range.addItems(queryCalls, first3Edges, pageInfo);
    expect(range.getSegmentedEdgeIDs()).toEqual([
      [edge1.__dataID__, edge2.__dataID__, edge3.__dataID__],
      [],
    ]);
  });
});