Home Reference Source Repository

src/mutation/__tests__/RelayMutation-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';

require('configureForRelayOSS');

jest
  .unmock('RelayMutation');

const Relay = require('Relay');
const RelayEnvironment = require('RelayEnvironment');
const RelayQuery = require('RelayQuery');
const RelayTestUtils = require('RelayTestUtils');

const buildRQL = require('buildRQL');

describe('RelayMutation', function() {
  let mockBarFragment;
  let mockFooFragment;
  let mockMutation;
  let environment;

  const {getNode, getPointer} = RelayTestUtils;

  function applyUpdate(mutation) {
    /* eslint-disable no-shadow */
    const RelayEnvironment = require.requireActual('RelayEnvironment');
    const environment = new RelayEnvironment();
    environment.applyUpdate(mutation);
    /* eslint-enable no-shadow */
  }

  beforeEach(function() {
    jest.resetModuleRegistry();

    environment = new RelayEnvironment();
    environment.read = jest.fn();

    const initialVariables = {isRelative: false};
    const makeMockMutation = () => {
      class MockMutationClass extends Relay.Mutation {
        static initialVariables = initialVariables;
        static fragments = {
          foo: () => Relay.QL`
            fragment on Comment {
              url(relative: $isRelative)
            }
          `,
          bar: () => Relay.QL`
            fragment on Node {
              id
            }
          `,
        };

        getConfigs() {
          return [];
        }
      }
      return MockMutationClass;
    };
    const MockMutation = makeMockMutation();

    const mockFooRequiredFragment =
      MockMutation.getFragment('foo').getFragment({});
    const mockBarRequiredFragment =
      MockMutation.getFragment('bar').getFragment({});
    const mockFooPointer = getPointer('foo', getNode(mockFooRequiredFragment));
    const mockBarPointer = getPointer('bar', getNode(mockBarRequiredFragment));

    // RelayMetaRoute.get(...)
    const mockRoute = {name: '$RelayMutation_MockMutationClass'};

    mockMutation = new MockMutation({
      bar: mockBarPointer,
      foo: mockFooPointer,
    });
    /* eslint-enable no-new */
    mockFooFragment = RelayQuery.Fragment.create(
      buildRQL.Fragment(MockMutation.fragments.foo, initialVariables),
      mockRoute,
      initialVariables
    );
    mockBarFragment = RelayQuery.Fragment.create(
      buildRQL.Fragment(MockMutation.fragments.bar, initialVariables),
      mockRoute,
      initialVariables
    );

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

  it('throws if used in different Relay environments', () => {
    mockMutation.bindEnvironment(environment);
    expect(() => {
      mockMutation.bindEnvironment(new RelayEnvironment());
    }).toFailInvariant(
      'MockMutationClass: Mutation instance cannot be used ' +
      'in different Relay environments.'
    );
  });

  it('can be reused in the same Relay environment', () => {
    mockMutation.bindEnvironment(environment);
    expect(() => {
      mockMutation.bindEnvironment(environment);
    }).not.toThrow();
  });

  it('does not resolve props before binding Relay environment', () => {
    expect(mockMutation.props).toBeUndefined();
  });

  it('resolves props only once', () => {
    mockMutation.bindEnvironment(environment);
    mockMutation.bindEnvironment(environment);
    expect(environment.read.mock.calls).toEqual([
      [/* fragment */mockFooFragment, /* dataID */'foo'],
      [/* fragment */mockBarFragment, /* dataID */'bar'],
    ]);
  });

  it('resolves props after binding Relay environment', () => {
    const resolvedProps = {
      bar: {},
      foo: {},
    };
    environment.read.mockImplementation((_, dataID) => resolvedProps[dataID]);
    mockMutation.bindEnvironment(environment);
    expect(environment.read.mock.calls).toEqual([
      [/* fragment */mockFooFragment, /* dataID */'foo'],
      [/* fragment */mockBarFragment, /* dataID */'bar'],
    ]);
    expect(mockMutation.props).toEqual(resolvedProps);
    expect(mockMutation.props.bar).toBe(resolvedProps.bar);
    expect(mockMutation.props.foo).toBe(resolvedProps.foo);
  });

  it('throws if mutation defines invalid `Relay.QL` fragment', () => {
    class BadMutation extends Relay.Mutation {}
    BadMutation.fragments = {
      foo: () => Relay.QL`query{node(id:"123"){id}}`,
    };
    const badFragmentReference = BadMutation.getFragment('foo');
    expect(() => {
      badFragmentReference.getFragment();
    }).toFailInvariant(
      'Relay.QL defined on mutation `BadMutation` named `foo` is not a valid ' +
      'fragment. A typical fragment is defined using: ' +
      'Relay.QL`fragment on Type {...}`'
    );
  });

  it('validates mutation configs when applied', () => {
    class MisconfiguredMutation extends Relay.Mutation {
      getConfigs() {
        return [{
          type: 'FIELDS_CHANGE',
          fieldIDS: ['4'],
        }];
      }
    }

    // Can't validate at construction time because we haven't resolved props
    // yet, and the config may depend on those.
    expect(() => new MisconfiguredMutation({})).not.toThrow();

    expect(() => applyUpdate(new MisconfiguredMutation({})))
      .toFailInvariant(
        'validateMutationConfig: Unexpected key `fieldIDS` in ' +
        '`FIELDS_CHANGE` config for `MisconfiguredMutation`; did you mean ' +
        '`fieldIDs`?'
      );
  });

  it('complains if mutation configs are not provided', () => {
    class UnconfiguredMutation extends Relay.Mutation {}

    // Can't validate at construction time because we haven't resolved props
    // yet, and the config may depend on those.
    expect(() => new UnconfiguredMutation({})).not.toThrow();

    expect(() => applyUpdate(new UnconfiguredMutation({})))
      .toThrowError(
        'UnconfiguredMutation: Expected abstract method `getConfigs` to be ' +
        'implemented.'
      );
  });
});