src/container/__tests__/RelayContainer-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('RelayContainerComparators')
.mock('warning');
const GraphQLStoreQueryResolver = require('GraphQLStoreQueryResolver');
const GraphQLStoreTestUtils = require('GraphQLStoreTestUtils');
const QueryBuilder = require('QueryBuilder');
const React = require('React');
const ReactTestUtils = require('ReactTestUtils');
const Relay = require('Relay');
const RelayEnvironment = require('RelayEnvironment');
const RelayMutation = require('RelayMutation');
const RelayQuery = require('RelayQuery');
const RelayRoute = require('RelayRoute');
const RelayTestUtils = require('RelayTestUtils');
const warning = require('warning');
describe('RelayContainer', function() {
let MockContainer;
let MockComponent;
let RelayTestRenderer;
let environment;
let mockBarPointer;
let mockFooFragment;
let mockFooPointer;
let mockRoute;
let render;
const {getNode, getPointer} = RelayTestUtils;
beforeEach(function() {
jest.resetModuleRegistry();
render = jest.fn(function() {
// Make it easier to expect prop values.
render.mock.calls[render.mock.calls.length - 1].props = this.props;
return <div />;
});
MockComponent = React.createClass({render});
MockContainer = Relay.createContainer(MockComponent, {
fragments: {
foo: jest.fn(
() => Relay.QL`fragment on Node{id,name}`
),
bar: jest.fn(
() => Relay.QL`fragment on Node @relay(plural:true){id,name}`
),
},
});
MockContainer.mock = {render};
environment = new RelayEnvironment();
mockRoute = RelayRoute.genMockInstance();
mockFooFragment = getNode(MockContainer.getFragment('foo').getFragment({}));
mockFooPointer = getPointer('42', mockFooFragment);
const mockBarFragment =
getNode(MockContainer.getFragment('bar').getFragment());
mockBarPointer = getPointer('42', mockBarFragment);
RelayTestRenderer = RelayTestUtils.createRenderer();
// TODO: #6524377 - migrate to RelayTestUtils matchers
jasmine.addMatchers(GraphQLStoreTestUtils.matchers);
jasmine.addMatchers(RelayTestUtils.matchers);
});
describe('fragments', () => {
it('throws if fragments are missing from spec', () => {
expect(() => {
Relay.createContainer(MockComponent, {});
}).toFailInvariant(
'Relay.createContainer(MockComponent, ...): Missing `fragments`, ' +
'which is expected to be an object mapping from `propName` to: ' +
'() => Relay.QL`...`'
);
});
it('throws if container defines invalid `Relay.QL` fragment', () => {
const BadContainer = Relay.createContainer(MockComponent, {
fragments: {
viewer: () => Relay.QL`query{node(id:"123"){id}}`,
},
});
const badFragmentReference = BadContainer.getFragment('viewer');
expect(() => {
badFragmentReference.getFragment();
}).toFailInvariant(
'Relay.QL defined on container `Relay(MockComponent)` named `viewer` ' +
'is not a valid fragment. A typical fragment is defined using: ' +
'Relay.QL`fragment on Type {...}`'
);
});
it('throws if container defines a fragment without function', () => {
const BadContainer = Relay.createContainer(MockComponent, {
fragments: {
viewer: Relay.QL`
fragment on Viewer {
newsFeed
}
`,
},
});
expect(() => {
BadContainer.getFragment('viewer');
}).toFailInvariant(
'RelayContainer: Expected `Relay(MockComponent).fragments.viewer` to ' +
'be a function returning a fragment. Example: ' +
'`viewer: () => Relay.QL`fragment on ...`'
);
});
it('throws if fragment and variable names are not unique', () => {
Relay.createContainer(MockComponent, {
initialVariables: {
badName: '100',
},
fragments: {
badName: () => Relay.QL`
fragment on Actor {
profilePicture(size:$badName) {
uri
}
}
`,
},
});
expect([
'Relay.createContainer(%s, ...): `%s` is used both ' +
'as a fragment name and variable name. Please give them unique names.',
'MockComponent',
'badName',
]).toBeWarnedNTimes(1);
});
it('creates query for a container without fragments', () => {
// Test that scalar constants are substituted, not only query fragments.
const MockProfilePhoto = Relay.createContainer(MockComponent, {
initialVariables: {
testPhotoSize: '100',
},
fragments: {
photo: () => Relay.QL`
fragment on Actor {
profilePicture(size:$testPhotoSize) {
uri
}
}
`,
},
});
const fragment = getNode(
MockProfilePhoto.getFragment('photo'),
{}
);
expect(fragment).toEqualQueryNode(getNode(Relay.QL`
fragment on Actor {
profilePicture(size: "100") {
uri
}
}
`));
});
it('creates query for a container with fragments', () => {
const anotherComponent = React.createClass({render: () => null});
const MockProfile = Relay.createContainer(MockComponent, {
fragments: {
user: () => Relay.QL`
fragment on Actor {
id
name
${MockProfileLink.getFragment('user')}
}
`,
},
});
const MockProfileLink = Relay.createContainer(anotherComponent, {
fragments: {
user: () => Relay.QL`
fragment on Actor {
id
url
}
`,
},
});
const fragment = getNode(
MockProfile.getFragment('user'),
{}
);
expect(fragment).toEqualQueryNode(getNode(Relay.QL`
fragment on Actor {
id
__typename
name
${Relay.QL`
fragment on Actor {
id,
__typename,
url,
}
`},
}
`));
});
it('returns whether a named fragment is defined', () => {
expect(MockContainer.hasFragment('foo')).toBe(true);
expect(MockContainer.hasFragment('bar')).toBe(true);
expect(MockContainer.hasFragment('baz')).toBe(false);
});
});
describe('conditional fragments', () => {
let MockProfile;
let profileFragment;
beforeEach(() => {
MockProfile = Relay.createContainer(MockComponent, {
fragments: {
viewer: () => Relay.QL`
fragment on Viewer {
primaryEmail
}
`,
},
});
profileFragment = QueryBuilder.createFragment({
name: 'Test',
type: 'Viewer',
children: [QueryBuilder.createField({fieldName: 'primaryEmail'})],
});
});
it('can conditionally include a fragment based on variables', () => {
const MockSideshow = Relay.createContainer(MockComponent, {
initialVariables: {
hasSideshow: null,
},
fragments: {
viewer: variables => Relay.QL`
fragment on Viewer {
${MockProfile.getFragment('viewer').if(variables.hasSideshow)}
}
`,
},
});
// hasSideshow: true
let fragment = getNode(
MockSideshow.getFragment('viewer', {
hasSideshow: QueryBuilder.createCallVariable('sideshow'),
}),
{sideshow: true}
);
const expected = RelayQuery.Fragment.build(
'Test',
'Viewer',
[getNode(profileFragment)]
);
expect(fragment).toEqualQueryNode(expected);
// hasSideshow: false
fragment = getNode(
MockSideshow.getFragment('viewer', {
hasSideshow: QueryBuilder.createCallVariable('sideshow'),
}),
{sideshow: false}
);
expect(fragment.getChildren().length).toBe(0);
});
it('can conditionally exclude a fragment based on variables', () => {
const MockSideshow = Relay.createContainer(MockComponent, {
initialVariables: {
hasSideshow: null,
},
fragments: {
viewer: variables => Relay.QL`
fragment on Viewer {
${MockProfile
.getFragment('viewer')
.unless(variables.hasSideshow)}
}
`,
},
});
// hasSideshow: true
let fragment = getNode(
MockSideshow.getFragment('viewer', {hasSideshow: true}),
{}
);
expect(fragment.getChildren().length).toBe(0);
// hasSideshow: false
fragment = getNode(
MockSideshow.getFragment('viewer', {hasSideshow: false}),
{}
);
const expected = RelayQuery.Fragment.build(
'Test',
'Viewer',
[getNode(profileFragment)],
);
expect(fragment).toEqualQueryNode(expected);
});
});
it('throws if rendered without a relay context', () => {
const ShallowRenderer = ReactTestUtils.createRenderer();
expect(() => ShallowRenderer.render(
<MockContainer foo={mockFooPointer} />
)).toFailInvariant(
'RelayContainer: `Relay(MockComponent)` was rendered with invalid ' +
'Relay context `undefined`. Make sure the `relay` property on the ' +
'React context conforms to the `RelayEnvironment` interface.'
);
});
it('throws if rendered with an invalid relay context', () => {
const fakeContext = {
getStoreData: null,
getFragmentResolver: null,
};
const ShallowRenderer = ReactTestUtils.createRenderer();
expect(() => ShallowRenderer.render(
<MockContainer foo={mockFooPointer} />,
{relay: fakeContext}
)).toFailInvariant(
'RelayContainer: `Relay(MockComponent)` was rendered with invalid ' +
'Relay context `[object Object]`. Make sure the `relay` property on ' +
'the React context conforms to the `RelayEnvironment` interface.'
);
});
it('throws if rendered without a route', () => {
const ShallowRenderer = ReactTestUtils.createRenderer();
expect(() => ShallowRenderer.render(
<MockContainer foo={mockFooPointer} />,
{relay: environment}
)).toFailInvariant(
'RelayContainer: `Relay(MockComponent)` was rendered without a valid ' +
'route. Make sure the route is valid, and make sure that it is ' +
'correctly set on the parent component\'s context ' +
'(e.g. using <RelayRootContainer>).'
);
});
describe('props.relay.variables', () => {
it('starts with initial variables', () => {
MockContainer = Relay.createContainer(MockComponent, {
initialVariables: {
public: 'instance',
private: 'instance',
},
fragments: {
foo: jest.fn(
() => Relay.QL`fragment on Node{id,name}`
),
},
});
MockContainer.mock = {render};
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environment,
mockRoute
);
const props = MockContainer.mock.render.mock.calls[0].props;
expect(props.relay.variables).toEqual({
public: 'instance',
private: 'instance',
});
});
it('starts with initial + parent variables', () => {
MockContainer = Relay.createContainer(MockComponent, {
initialVariables: {
public: 'instance',
private: 'instance',
},
fragments: {
foo: jest.fn(
() => Relay.QL`fragment on Node{id,name}`
),
},
});
MockContainer.mock = {render};
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} public="parent" />,
environment,
mockRoute
);
const props = MockContainer.mock.render.mock.calls[0].props;
expect(props.relay.variables).toEqual({
public: 'parent',
private: 'instance',
});
});
it('starts with prepared initial variables', () => {
const prepareVariables = jest.fn((vars, route) => {
expect(route.name).toBe(mockRoute.name);
return {
...vars,
private: 'prepared',
};
});
MockContainer = Relay.createContainer(MockComponent, {
initialVariables: {
public: 'instance',
private: 'instance',
},
prepareVariables,
fragments: {
foo: jest.fn(
() => Relay.QL`fragment on Node{id,name}`
),
},
});
MockContainer.mock = {render};
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environment,
mockRoute
);
const props = MockContainer.mock.render.mock.calls[0].props;
expect(props.relay.variables).toEqual({
public: 'instance',
private: 'prepared',
});
});
it('starts with prepared initial + parent variables', () => {
const prepareVariables = jest.fn((vars, route) => {
expect(route.name).toBe(mockRoute.name);
return {
...vars,
private: 'prepared',
};
});
MockContainer = Relay.createContainer(MockComponent, {
initialVariables: {
public: 'instance',
private: 'instance',
},
prepareVariables,
fragments: {
foo: jest.fn(
() => Relay.QL`fragment on Node{id,name}`
),
},
});
MockContainer.mock = {render};
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} public="parent" />,
environment,
mockRoute
);
const props = MockContainer.mock.render.mock.calls[0].props;
expect(props.relay.variables).toEqual({
public: 'parent',
private: 'prepared',
});
});
it('updates with prepared initial + parent variables', () => {
const prepareVariables = jest.fn((vars, route) => {
expect(route.name).toBe(mockRoute.name);
return {
...vars,
private: 'prepared1',
};
});
MockContainer = Relay.createContainer(MockComponent, {
initialVariables: {
public: 'instance',
private: 'instance',
},
prepareVariables,
fragments: {
foo: jest.fn(
() => Relay.QL`fragment on Node{id,name}`
),
},
});
MockContainer.mock = {render};
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} public="parent1" />,
environment,
mockRoute
);
const props = MockContainer.mock.render.mock.calls[0].props;
expect(props.relay.variables).toEqual({
public: 'parent1',
private: 'prepared1',
});
prepareVariables.mockImplementation(vars => ({
...vars,
private: 'prepared2',
}));
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} public="parent2" />,
environment,
mockRoute
);
const nextProps = MockContainer.mock.render.mock.calls[1].props;
expect(nextProps.relay.variables).toEqual({
public: 'parent2',
private: 'prepared2',
});
});
});
describe('props.relay.applyUpdate', () => {
it('forwards to the underlying RelayEnvironment', () => {
const mockMutation = new RelayMutation();
environment.applyUpdate = jest.fn();
render.mockImplementation(function() {
this.props.relay.applyUpdate(mockMutation);
});
RelayTestRenderer.render(
() => <MockContainer />,
environment,
mockRoute
);
expect(environment.applyUpdate.mock.calls[0][0]).toBe(mockMutation);
});
});
describe('props.relay.commitUpdate', () => {
it('forwards to the underlying RelayEnvironment', () => {
const mockMutation = new RelayMutation();
environment.commitUpdate = jest.fn();
render.mockImplementation(function() {
this.props.relay.commitUpdate(mockMutation);
});
RelayTestRenderer.render(
() => <MockContainer />,
environment,
mockRoute
);
expect(environment.commitUpdate.mock.calls[0][0]).toBe(mockMutation);
});
});
it('creates resolvers for each query prop with a fragment pointer', () => {
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environment,
mockRoute
);
expect(environment.getFragmentResolver.mock.calls.length).toBe(1);
expect(GraphQLStoreQueryResolver.mock.instances.length).toBe(1);
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} bar={[mockBarPointer]} />,
environment,
mockRoute
);
// `foo` resolver is re-used, `bar` is added
expect(environment.getFragmentResolver.mock.calls.length).toBe(2);
expect(GraphQLStoreQueryResolver.mock.instances.length).toBe(2);
});
it('recreates resolvers when relay context changes', () => {
const environmentA = new RelayEnvironment();
const environmentB = new RelayEnvironment();
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environmentA,
mockRoute
);
expect(environmentA.getFragmentResolver.mock.calls.length).toBe(1);
const mockResolvers = GraphQLStoreQueryResolver.mock.instances;
expect(mockResolvers.length).toBe(1);
expect(mockResolvers[0].dispose).not.toBeCalled();
environmentA.getFragmentResolver.mockClear();
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environmentB,
mockRoute
);
expect(environmentA.getFragmentResolver.mock.calls.length).toBe(0);
expect(environmentB.getFragmentResolver.mock.calls.length).toBe(1);
expect(mockResolvers.length).toBe(2);
expect(mockResolvers[1].mock.store).toBe(environmentB.getStoreData());
expect(mockResolvers[0].dispose).toBeCalled();
expect(mockResolvers[1].dispose).not.toBeCalled();
});
it('reuses resolvers even if route changes', () => {
const MockRouteA = RelayRoute.genMock();
const MockRouteB = RelayRoute.genMock();
const mockRouteA = new MockRouteA();
const mockRouteB = new MockRouteB();
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environment,
mockRouteA
);
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environment,
mockRouteB
);
expect(environment.getFragmentResolver.mock.calls.length).toBe(1);
expect(GraphQLStoreQueryResolver.mock.instances.length).toBe(1);
expect(GraphQLStoreQueryResolver.mock.instances[0].dispose).not.toBeCalled();
});
it('resolves each prop with a query', () => {
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environment,
mockRoute
);
const fragment = getNode(MockContainer.getFragment('foo'));
expect(environment.getFragmentResolver.mock.calls.length).toBe(1);
const mockResolvers = GraphQLStoreQueryResolver.mock.instances;
expect(mockResolvers.length).toBe(1);
expect(mockResolvers[0].resolve.mock.calls[0][0])
.toEqualQueryNode(fragment);
expect(mockResolvers[0].resolve.mock.calls[0][1])
.toEqual(mockFooPointer.__dataID__);
});
it('re-resolves props when notified of changes', () => {
const mockData = {__dataID__: '42', id: '42', name: 'Tim'};
GraphQLStoreQueryResolver.mockDefaultResolveImplementation(() => mockData);
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environment,
mockRoute
);
expect(environment.getFragmentResolver.mock.calls.length).toBe(1);
const mockResolvers = GraphQLStoreQueryResolver.mock.instances;
mockResolvers[0].mock.callback();
expect(mockResolvers.length).toBe(1);
expect(mockResolvers[0].dispose.mock.calls.length).toBe(0);
expect(mockResolvers[0].resolve.mock.calls.length).toBe(2);
});
it('re-resolves props when relay context changes', () => {
const environmentA = new RelayEnvironment();
const environmentB = new RelayEnvironment();
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environmentA,
mockRoute
);
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environmentB,
mockRoute
);
const mockResolvers = GraphQLStoreQueryResolver.mock.instances;
expect(mockResolvers.length).toBe(2);
expect(mockResolvers[0].resolve.mock.calls.length).toBe(1);
expect(mockResolvers[1].resolve.mock.calls.length).toBe(1);
});
it('re-resolves props when route changes', () => {
const MockRouteA = RelayRoute.genMock();
const MockRouteB = RelayRoute.genMock();
const mockRouteA = new MockRouteA();
const mockRouteB = new MockRouteB();
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environment,
mockRouteA
);
expect(environment.getFragmentResolver.mock.calls.length).toBe(1);
const mockResolvers = GraphQLStoreQueryResolver.mock.instances;
expect(mockResolvers.length).toBe(1);
expect(mockResolvers[0].resolve.mock.calls.length).toBe(1);
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environment,
mockRouteB
);
expect(mockResolvers.length).toBe(1);
expect(mockResolvers[0].resolve.mock.calls.length).toBe(2);
});
it('updates relay.route when route changes', () => {
const MockRouteA = RelayRoute.genMock();
const MockRouteB = RelayRoute.genMock();
const mockRouteA = new MockRouteA();
const mockRouteB = new MockRouteB();
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environment,
mockRouteA
);
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environment,
mockRouteB
);
const routeAProps = MockContainer.mock.render.mock.calls[0].props;
const routeBProps = MockContainer.mock.render.mock.calls[1].props;
expect(routeAProps.relay.route).toBe(mockRouteA);
expect(routeBProps.relay.route).toBe(mockRouteB);
});
it('resolves with most recent props', () => {
const fooFragment = getNode(MockContainer.getFragment('foo'));
const mockPointerA = getPointer('42', mockFooFragment);
const mockPointerB = getPointer('43', mockFooFragment);
RelayTestRenderer.render(
() => <MockContainer foo={mockPointerA} />,
environment,
mockRoute
);
RelayTestRenderer.render(
() => <MockContainer foo={mockPointerB} />,
environment,
mockRoute
);
const mockResolvers = GraphQLStoreQueryResolver.mock.instances;
expect(mockResolvers.length).toBe(1);
expect(mockResolvers[0].dispose.mock.calls.length).toBe(0);
expect(mockResolvers[0].resolve.mock.calls.length).toBe(2);
expect(mockResolvers[0].resolve.mock.calls[0][0])
.toEqualQueryNode(fooFragment);
expect(mockResolvers[0].resolve.mock.calls[0][1])
.toEqual(mockPointerA.__dataID__);
expect(mockResolvers[0].resolve.mock.calls[1][0])
.toEqualQueryNode(fooFragment);
expect(mockResolvers[0].resolve.mock.calls[1][1])
.toEqual(mockPointerB.__dataID__);
});
it('does not create resolvers for null/undefined props', () => {
RelayTestRenderer.render(
() => <MockContainer foo={null} bar={undefined} />,
environment,
mockRoute
);
expect(environment.getFragmentResolver.mock.calls.length).toBe(0);
const mockResolvers = GraphQLStoreQueryResolver.mock.instances;
expect(mockResolvers.length).toBe(0);
const props = MockContainer.mock.render.mock.calls[0].props;
expect(props.bar).toBe(undefined);
expect(props.foo).toBe(null);
});
it('warns if props are missing fragment pointers', () => {
const mockData = {};
RelayTestRenderer.render(
() => <MockContainer foo={mockData} bar={null} />,
environment,
mockRoute
);
const mockResolvers = GraphQLStoreQueryResolver.mock.instances;
expect(mockResolvers.length).toBe(0);
const props = MockContainer.mock.render.mock.calls[0].props;
expect(props.bar).toBe(null);
expect(props.foo).toBe(mockData);
expect(warning.mock.calls.filter(call =>
call[0] === false && call[1].indexOf(
'RelayContainer: component `%s` was rendered with variables ' +
'that differ from the variables used to fetch fragment ' +
'`%s`.'
) === 0
).length).toBe(1);
});
it('warns if fragment pointer exists on a different prop', () => {
mockFooPointer = getPointer('42', mockFooFragment);
RelayTestRenderer.render(
() => <MockContainer baz={mockFooPointer} />,
environment,
mockRoute
);
expect([
'RelayContainer: Expected record data for prop `%s` on `%s`, ' +
'but it was instead on prop `%s`. Did you misspell a prop or ' +
'pass record data into the wrong prop?',
'foo',
'MockComponent',
'baz',
]).toBeWarnedNTimes(1);
});
it('does not warn if fragment hash exists on a different prop', () => {
const deceptiveArray = [];
deceptiveArray[Object.keys(mockFooPointer)[0]] = {};
RelayTestRenderer.render(
() => <MockContainer baz={deceptiveArray} />,
environment,
mockRoute
);
expect([
'RelayContainer: Expected record data for prop `%s` on `%s`, ' +
'but it was instead on prop `%s`. Did you misspell a prop or ' +
'pass record data into the wrong prop?',
'foo',
'MockComponent',
'baz',
]).toBeWarnedNTimes(0);
});
it('warns if a fragment is not passed in', () => {
RelayTestRenderer.render(
() => <MockContainer foo={null} />,
environment,
mockRoute
);
const mockResolvers = GraphQLStoreQueryResolver.mock.instances;
expect(mockResolvers.length).toBe(0);
const props = MockContainer.mock.render.mock.calls[0].props;
expect(props.bar).toBe(undefined);
expect(props.foo).toBe(null);
expect([
'RelayContainer: Expected prop `%s` to be supplied to `%s`, but ' +
'got `undefined`. Pass an explicit `null` if this is intentional.',
'bar',
'MockComponent',
]).toBeWarnedNTimes(1);
});
it('warns if a fragment prop is not an object', () => {
RelayTestRenderer.render(
() => <MockContainer foo={''} />,
environment,
mockRoute
);
const mockResolvers = GraphQLStoreQueryResolver.mock.instances;
expect(mockResolvers.length).toBe(0);
const props = MockContainer.mock.render.mock.calls[0].props;
expect(props.bar).toBe(undefined);
expect(props.foo).toBe('');
expect([
'RelayContainer: Expected prop `%s` supplied to `%s` to be an ' +
'object, got `%s`.',
'foo',
'MockComponent',
'',
]).toBeWarnedNTimes(1);
});
it('throws if non-plural fragment receives an array', () => {
const mockData = [];
expect(() => {
RelayTestRenderer.render(
() => <MockContainer foo={mockData} />,
environment,
mockRoute
);
}).toFailInvariant(
'RelayContainer: Invalid prop `foo` supplied to `MockComponent`, ' +
'expected a single record because the corresponding fragment is not ' +
'plural (i.e. does not have `@relay(plural: true)`).'
);
});
it('throws if plural fragment receives a non-array', () => {
const mockData = {};
expect(() => {
RelayTestRenderer.render(
() => <MockContainer bar={mockData} />,
environment,
mockRoute
);
}).toFailInvariant(
'RelayContainer: Invalid prop `bar` supplied to `MockComponent`, ' +
'expected an array of records because the corresponding fragment has ' +
'`@relay(plural: true)`.'
);
});
it('warns if plural fragment array item is missing fragment pointers', () => {
const mockData = [{}];
RelayTestRenderer.render(
() => <MockContainer bar={mockData} />,
environment,
mockRoute
);
expect(warning.mock.calls.filter(call =>
call[0] === false && call[1].indexOf(
'RelayContainer: component `%s` was rendered with variables ' +
'that differ from the variables used to fetch fragment ' +
'`%s`.'
) === 0
).length).toBe(1);
});
it('throws if some plural fragment items are null', () => {
const mockData = [mockBarPointer, null];
expect(() => {
RelayTestRenderer.render(
() => <MockContainer bar={mockData} />,
environment,
mockRoute
);
}).toFailInvariant(
'RelayContainer: Invalid prop `bar` supplied to `MockComponent`. Some ' +
'array items contain data fetched by Relay and some items contain ' +
'null/mock data.'
);
});
it('throws if some but not all plural fragment items are mocked', () => {
const mockData = [mockBarPointer, {}];
expect(() => {
RelayTestRenderer.render(
() => <MockContainer bar={mockData} />,
environment,
mockRoute
);
}).toFailInvariant(
'RelayContainer: Invalid prop `bar` supplied to `MockComponent`. Some ' +
'array items contain data fetched by Relay and some items contain ' +
'null/mock data.'
);
});
it('passes through empty arrays for plural fragments', () => {
RelayTestRenderer.render(
() => <MockContainer bar={[]} />,
environment,
mockRoute
);
expect(MockContainer.mock.render.mock.calls.length).toBe(1);
expect(MockContainer.mock.render.mock.calls[0].props.bar).toEqual([]);
expect(environment.getFragmentResolver).not.toBeCalled();
});
it('does not re-render if props resolve to the same object', () => {
const mockData = {__dataID__: '42', id: '42', name: 'Tim'};
GraphQLStoreQueryResolver.mockDefaultResolveImplementation(() => mockData);
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environment,
mockRoute
);
expect(MockContainer.mock.render.mock.calls.length).toBe(1);
expect(MockContainer.mock.render.mock.calls[0].props.foo).toEqual(mockData);
GraphQLStoreQueryResolver.mock.instances[0].mock.callback();
expect(MockContainer.mock.render.mock.calls.length).toBe(1);
});
it('re-renders if props resolve to different objects', () => {
const mockDataList = [
{__dataID__: '42', id: '42', name: 'Tim', ...mockFooPointer},
{__dataID__: '42', id: '42', name: 'Tee', ...mockFooPointer},
];
GraphQLStoreQueryResolver.mockResolveImplementation(0, function() {
return mockDataList[this.resolve.mock.calls.length - 1];
});
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
environment,
mockRoute
);
expect(MockContainer.mock.render.mock.calls.length).toBe(1);
expect(MockContainer.mock.render.mock.calls[0].props.foo).toEqual(
mockDataList[0]
);
GraphQLStoreQueryResolver.mock.instances[0].mock.callback();
expect(MockContainer.mock.render.mock.calls.length).toBe(2);
expect(MockContainer.mock.render.mock.calls[1].props.foo).toEqual(
mockDataList[1]
);
});
it('renders with identical props if no data has changed', () => {
// Non-scalars deoptimize `RelayContainer.shouldComponentUpdate`.
const nonScalar = {};
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} deopt={nonScalar} />,
environment,
mockRoute
);
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} deopt={nonScalar} />,
environment,
mockRoute
);
expect(MockContainer.mock.render.mock.calls.length).toBe(2);
const propsA = MockContainer.mock.render.mock.calls[0].props;
const propsB = MockContainer.mock.render.mock.calls[1].props;
const propNamesA = Object.keys(propsA);
const propNamesB = Object.keys(propsB);
// Fix for V8 bug where insertion order isn't preserved (see #10804655)
expect(propNamesA.sort()).toEqual(propNamesB.sort());
propNamesA.forEach(propName => {
expect(propsA[propName]).toBe(propsB[propName]);
});
});
it('applies `shouldComponentUpdate` properly', () => {
const mockDataSet = {
'42': {__dataID__: '42', name: 'Tim'},
'43': {__dataID__: '43', name: 'Tee'},
'44': {__dataID__: '44', name: 'Toe'},
};
render = jest.fn(() => <div />);
const shouldComponentUpdate = jest.fn();
const MockFastComponent = React.createClass({render, shouldComponentUpdate});
const MockFastContainer = Relay.createContainer(MockFastComponent, {
fragments: {
foo: jest.fn(
() => Relay.QL`fragment on Node{id,name}`
),
},
});
GraphQLStoreQueryResolver.mockResolveImplementation(0, (_, dataID) => {
return mockDataSet[dataID];
});
mockFooFragment =
getNode(MockFastContainer.getFragment('foo').getFragment({}));
const mockPointerA = getPointer('42', mockFooFragment);
const mockPointerB = getPointer('43', mockFooFragment);
const mockPointerC = getPointer('44', mockFooFragment);
RelayTestRenderer.render(
() => <MockFastContainer foo={mockPointerA} />,
environment,
mockRoute
);
expect(render.mock.calls.length).toBe(1);
shouldComponentUpdate.mockReturnValue(true);
// Component wants to update, RelayContainer doesn't.
RelayTestRenderer.render(
() => <MockFastContainer foo={mockPointerA} />,
environment,
mockRoute
);
expect(render.mock.calls.length).toBe(1);
// Component wants to update, RelayContainer does too.
RelayTestRenderer.render(
() => <MockFastContainer foo={mockPointerB} />,
environment,
mockRoute
);
expect(render.mock.calls.length).toBe(2);
shouldComponentUpdate.mockReturnValue(false);
// Component doesn't want to update, RelayContainer does.
RelayTestRenderer.render(
() => <MockFastContainer foo={mockPointerC} />,
environment,
mockRoute
);
expect(render.mock.calls.length).toBe(2);
// Component doesn't want to update, RelayContainer doesn't either.
RelayTestRenderer.render(
() => <MockFastContainer foo={mockPointerC} />,
environment,
mockRoute
);
expect(render.mock.calls.length).toBe(2);
shouldComponentUpdate.mockReturnValue(true);
RelayTestRenderer.render(
() => <MockFastContainer foo={mockPointerC} thing="scalar" />,
environment,
mockRoute
);
expect(render.mock.calls.length).toBe(3);
});
it('applies `shouldComponentUpdate` properly', () => {
const mockDataSet = {
'42': {__dataID__: '42', name: 'Tim'},
};
render = jest.genMockFunction().mockImplementation(() => <div />);
const shouldComponentUpdate = jest.fn(() => true);
const MockAlwaysUpdateComponent = Relay.createContainer(
React.createClass({render, shouldComponentUpdate}),
{
shouldComponentUpdate,
fragments: {
foo: jest.genMockFunction().mockImplementation(
() => Relay.QL`fragment on Node{id,name}`
),
},
}
);
GraphQLStoreQueryResolver.mockResolveImplementation(0, (_, dataID) => {
return mockDataSet[dataID];
});
mockFooFragment =
getNode(MockAlwaysUpdateComponent.getFragment('foo').getFragment({}));
const mockPointerA = getPointer('42', mockFooFragment);
RelayTestRenderer.render(
() => <MockAlwaysUpdateComponent foo={mockPointerA} />,
environment,
mockRoute
);
expect(render.mock.calls.length).toBe(1);
RelayTestRenderer.render(
() => <MockAlwaysUpdateComponent foo={mockPointerA} />,
environment,
mockRoute
);
expect(shouldComponentUpdate.mock.calls.length).toBe(2);
expect(shouldComponentUpdate.mock.calls[0].length).toBe(0);
expect(render.mock.calls.length).toBe(2);
});
});