Home Reference Source Repository

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

const Relay = require('Relay');
const RelayConnectionInterface = require('RelayConnectionInterface');
const RelayOptimisticMutationUtils = require('RelayOptimisticMutationUtils');
const RelayQuery = require('RelayQuery');
const RelayTestUtils = require('RelayTestUtils');

const flattenRelayQuery = require('flattenRelayQuery');

describe('RelayOptimisticMutationUtils', () => {
  const {getVerbatimNode, matchers} = RelayTestUtils;
  let HAS_NEXT_PAGE, HAS_PREV_PAGE, PAGE_INFO;

  beforeEach(() => {
    jest.resetModuleRegistry();

    ({
      HAS_NEXT_PAGE,
      HAS_PREV_PAGE,
      PAGE_INFO,
    } = RelayConnectionInterface);

    jasmine.addMatchers({
      ...RelayTestUtils.matchers,
      toEqualFields() {
        return {
          compare(actual, expected) {
            expected = flattenRelayQuery(getVerbatimNode(expected));
            actual = flattenRelayQuery(expected.clone(actual));
            // NOTE: Generated fields might get in the way.
            return matchers.toEqualQueryNode().compare(actual, expected);
          },
        };
      },
    });
  });

  describe('inferRelayFieldsFromData', () => {
    it('generates metadata for fields', () => {
      const query = RelayOptimisticMutationUtils.inferRelayFieldsFromData({
        id: '123',
      });
      expect(query).toEqualFields(Relay.QL`
        fragment on Actor {
          id
        }
      `);
      expect(query[0].isPlural()).toBe(false);
    });

    it('infers scalar fields from scalars', () => {
      expect(RelayOptimisticMutationUtils.inferRelayFieldsFromData({
        id: '123',
        name: 'Alice',
      })).toEqualFields(Relay.QL`
        fragment on Actor {
          id
          name
        }
      `);
    });

    it('infers nested fields from objects', () => {
      const fields = RelayOptimisticMutationUtils.inferRelayFieldsFromData({
        id: '123',
        address: {
          city: 'Menlo Park',
        },
      });
      expect(fields).toEqualFields(Relay.QL`
        fragment on Actor {
          id
          address {
            city
          }
        }
      `);
      expect(fields[1].canHaveSubselections()).toBe(true);
    });

    it('infers unterminated fields from null', () => {
      const inferredFields = RelayOptimisticMutationUtils.inferRelayFieldsFromData({
        id: '123',
        address: null,
      });

      expect(inferredFields[0] instanceof RelayQuery.Field).toBe(true);
      expect(inferredFields[0].canHaveSubselections()).toBe(false);
      expect(inferredFields[0].getSchemaName()).toBe('id');

      expect(inferredFields[1] instanceof RelayQuery.Field).toBe(true);
      // Though this field can have subselections, there is no way we can infer
      // this from `address: null`. Defaults to false.
      expect(inferredFields[1].canHaveSubselections()).toBe(false);
      expect(inferredFields[1].getSchemaName()).toBe('address');
    });

    it('infers plural fields from arrays of scalars', () => {
      const fields = RelayOptimisticMutationUtils.inferRelayFieldsFromData({
        id: '123',
        websites: [
          'facebook.com',
          'google.com',
        ],
      });
      expect(fields).toEqualFields(Relay.QL`
        fragment on Actor {
          id
          websites
        }
      `);
      expect(fields[1].isPlural()).toBe(true);
    });

    it('infers plural nested fields from arrays of objects', () => {
      const fields = RelayOptimisticMutationUtils.inferRelayFieldsFromData({
        id: '123',
        screennames: [
          {service: 'GTALK'},
          {service: 'TWITTER'},
        ],
      });
      expect(fields).toEqualFields(Relay.QL`
        fragment on Actor {
          id
          screennames {
            service
          }
        }
      `);
      expect(fields[1].isPlural()).toBe(true);
    });

    it('infers unterminated fields from empty arrays', () => {
      expect(RelayOptimisticMutationUtils.inferRelayFieldsFromData({
        id: '123',
        websites: [],
      })).toEqualFields(Relay.QL`
        fragment on Actor {
          id
          websites
        }
      `);
    });

    it('infers unterminated fields from null elements in arrays', () => {
      expect(RelayOptimisticMutationUtils.inferRelayFieldsFromData({
        id: '123',
        websites: [null],
      })).toEqualFields(Relay.QL`
        fragment on Actor {
          id
          websites
        }
      `);
    });

    it('infers String field arguments from keys', () => {
      expect(RelayOptimisticMutationUtils.inferRelayFieldsFromData({
        id: '123',
        'url(site: "www")': 'https://...',
      })).toEqualFields(Relay.QL`
        fragment on Actor {
          id
          url(site: "www")
        }
      `);
    });

    it('infers Boolean field arguments from keys', () => {
      expect(RelayOptimisticMutationUtils.inferRelayFieldsFromData({
        'url(relative: true)': '//...',
      })).toEqualFields(Relay.QL`
        fragment on Actor {
          url(relative: true)
        }
      `);
    });

    it('infers Int field arguments from keys', () => {
      expect(RelayOptimisticMutationUtils.inferRelayFieldsFromData({
        'comments(last: 10)': {
          count: 20,
        },
      })).toEqualFields(Relay.QL`
        fragment on Comment {
          comments(last: 10) {
            count
          }
        }
      `);
    });

    it('throws for keys with invalid call encodings', () => {
      expect(() => {
        RelayOptimisticMutationUtils.inferRelayFieldsFromData({
          'url.site': 'https://...',
        });
      }).toFailInvariant(
        'RelayOptimisticMutationUtils: Malformed data key, `url.site`.'
      );

      expect(() => {
        RelayOptimisticMutationUtils.inferRelayFieldsFromData({
          'url(site)': 'https://...',
        });
      }).toFailInvariant(
        'RelayOptimisticMutationUtils: Malformed or unsupported data key, ' +
        '`url(site)`. Only booleans, strings, and numbers are currently ' +
        'supported, and commas are required. Parse failure reason was ' +
        '`' + RelayTestUtils.getJSONTokenError('s', 1) + '`.'
      );
    });

    it('infers `id` and `cursor` fields for `node` data', () => {
      expect(RelayOptimisticMutationUtils.inferRelayFieldsFromData({
        id: '123',
        'friends(first: "2")': {
          edges: [
            {node: {name: 'Alice'}},
            {node: {name: 'Bob'}},
          ],
          [PAGE_INFO]: {
            [HAS_NEXT_PAGE]: true,
            [HAS_PREV_PAGE]: false,
          },
        },
      })).toEqualFields(Relay.QL`
        fragment on Actor {
          id
          friends(first: "2") {
            edges {
              node {
                id
                name
              }
              cursor
            }
            pageInfo {
              hasNextPage
              hasPreviousPage
            }
          }
        }
      `);
    });

    it('infers field for mutation field named `node`', () => {
      expect(RelayOptimisticMutationUtils.inferRelayFieldsFromData({
        node: {
          id: '123',
          name: 'name',
        },
      })).toEqualFields(Relay.QL`
        fragment on NodeSavedStateResponsePayload {
          node {
            id
            name
          }
        }
      `);
    });

    it('ignores metadata fields', () => {
      expect(RelayOptimisticMutationUtils.inferRelayFieldsFromData({
        __dataID__: '123',
        __range__: null,
        __status__: 0,
        id: '123',
        name: 'Alice',
      })).toEqualFields(Relay.QL`
        fragment on Node {
          id
          name
        }
      `);
    });
  });

  describe('inferRelayPayloadFromData', () => {
    it('generates payload for scalar fields', () => {
      const data = {
        id: '123',
      };
      const payload =
        RelayOptimisticMutationUtils.inferRelayPayloadFromData(data);
      expect(payload).toBe(data);
    });

    it('generates payload for plural fields', () => {
      const data = {
        usernames: [{id: '123'}, {id: '456'}],
      };
      const payload =
        RelayOptimisticMutationUtils.inferRelayPayloadFromData(data);
      expect(payload).toBe(data);
    });

    it('generates payload for nested fields', () => {
      const data = {
        viewer: {
          actor: {
            id: '123',
          },
        },
      };
      const payload =
        RelayOptimisticMutationUtils.inferRelayPayloadFromData(data);
      expect(payload).toBe(data);
    });

    it('generates payload for String field arguments from keys', () => {
      const field = RelayQuery.Field.build({
        calls: [{name: 'site', value: 'www'}],
        fieldName: 'url',
      });

      expect(RelayOptimisticMutationUtils.inferRelayPayloadFromData({
        id: '123',
        'url(site: "www")': 'https://...',
      })).toEqual({
        id: '123',
        [field.getSerializationKey()]: 'https://...',
      });
    });

    it('generates payload Boolean field arguments from keys', () => {
      const field = RelayQuery.Field.build({
        calls: [{name: 'relative', value: true}],
        fieldName: 'url',
      });
      expect(RelayOptimisticMutationUtils.inferRelayPayloadFromData({
        'url(relative: true)': '//...',
      })).toEqual({
        [field.getSerializationKey()]: '//...',
      });
    });

    it('infers Int field arguments from keys', () => {
      const field = RelayQuery.Field.build({
        calls: [{name: 'last', value: 10}],
        fieldName: 'comments',
      });
      expect(RelayOptimisticMutationUtils.inferRelayPayloadFromData({
        'comments(last: 10)': {
          count: 20,
        },
      })).toEqual({
        [field.getSerializationKey()]: {
          count: 20,
        },
      });
    });

    it('throws for keys with invalid call encodings', () => {
      expect(() => {
        RelayOptimisticMutationUtils.inferRelayPayloadFromData({
          'url.site': 'https://...',
        });
      }).toFailInvariant(
        'RelayOptimisticMutationUtils: Malformed data key, `url.site`.'
      );

      expect(() => {
        RelayOptimisticMutationUtils.inferRelayPayloadFromData({
          'url(site)': 'https://...',
        });
      }).toFailInvariant(
        'RelayOptimisticMutationUtils: Malformed or unsupported data key, ' +
        '`url(site)`. Only booleans, strings, and numbers are currently ' +
        'supported, and commas are required. Parse failure reason was ' +
        '`' + RelayTestUtils.getJSONTokenError('s', 1) + '`.'
      );
    });
  });
});