src/container/__tests__/RelayContainer_setVariables-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';
jest.mock('warning');
require('configureForRelayOSS');
const GraphQLStoreQueryResolver = require('GraphQLStoreQueryResolver');
const QueryBuilder = require('QueryBuilder');
const React = require('React');
const ReactDOM = require('ReactDOM');
const Relay = require('Relay');
const RelayEnvironment = require('RelayEnvironment');
const RelayMetaRoute = require('RelayMetaRoute');
const RelayQuery = require('RelayQuery');
const RelayTestUtils = require('RelayTestUtils');
describe('RelayContainer.setVariables', function() {
let MockComponent;
let MockContainer;
let defaultState;
let domContainer;
let entityQuery;
let environment;
let mockInstance;
let prepareVariables;
let render;
const {getNode, getPointer} = RelayTestUtils;
beforeEach(function() {
jest.resetModuleRegistry();
const fragment = Relay.QL`fragment on Node{url(site:$site)}`;
entityQuery = jest.fn(() => fragment);
render = jest.fn(() => <div />);
prepareVariables = jest.fn(
(variables, route) => variables
);
// Make RQLTransform ignore this call.
MockComponent = React.createClass({render});
const createContainer = Relay.createContainer;
MockContainer = createContainer(MockComponent, {
fragments: {
entity: entityQuery,
},
initialVariables: {site: 'mobile'},
prepareVariables,
});
environment = new RelayEnvironment();
GraphQLStoreQueryResolver.mockDefaultResolveImplementation((_, dataID) => {
expect(dataID).toBe('42');
return {
__dataID__: '42',
__fragments__: {
[getNode(fragment).getConcreteFragmentID()]: '42',
},
id: '42',
url: '//url',
profilePicture: {
uri: '//url',
},
};
});
defaultState = {
aborted: false,
done: false,
error: null,
mounted: true,
ready: false,
stale: false,
};
domContainer = document.createElement('div');
mockInstance = RelayTestUtils.createRenderer(domContainer).render(
genMockPointer => <MockContainer entity={genMockPointer('42')} />,
environment
);
jasmine.addMatchers(RelayTestUtils.matchers);
});
describe('plural fragments', () => {
beforeEach(() => {
GraphQLStoreQueryResolver.mockDefaultResolveImplementation(pointer => {
return [{
__dataID__: '42',
id: '42',
url: '//url',
profilePicture: {
uri: '//url',
},
}];
});
const pluralEntityQuery = jest.fn(
() => Relay.QL`
fragment on Node @relay(plural:true) {
url(site: $site)
}
`
);
MockContainer = Relay.createContainer(MockComponent, {
fragments: {
entity: pluralEntityQuery,
},
initialVariables: {site: 'mobile'},
});
// Return an array
GraphQLStoreQueryResolver.mockDefaultResolveImplementation((_, ids) => {
expect(ids).toEqual(['21', '42']);
return [
{
__dataID__: '21',
id: '21',
url: '//url',
profilePicture: {
uri: '//url',
},
},
{
__dataID__: '42',
id: '42',
url: '//url',
profilePicture: {
uri: '//url',
},
},
];
});
const fragment = getNode(MockContainer.getFragment('entity').getFragment());
const mockPointers = [
getPointer('21', fragment),
getPointer('42', fragment),
];
mockInstance = RelayTestUtils.createRenderer(domContainer).render(
genMockPointer => (
<MockContainer entity={mockPointers} />
),
environment
);
});
it('creates multiple queries for plural fragments', () => {
jest.runAllTimers();
mockInstance.forceFetch();
expect(environment.forceFetch).toBeCalled();
const querySet = environment.forceFetch.mock.calls[0][0];
expect(Object.keys(querySet)).toEqual(['entity0', 'entity1']);
});
it('creates queries only for records with dataIDs', () => {
const updatedQueryData = [
{__dataID__: '21', id: '21', url: '//www'},
{id:'1336', name: 'Fake data', url: '//www'},
];
GraphQLStoreQueryResolver.mockDefaultResolveImplementation(pointer => {
return updatedQueryData;
});
// Change the query data that is stored by the container to
// `updatedQueryData`
mockInstance.forceFetch();
environment.forceFetch.mock.requests[0].succeed();
mockInstance.forceFetch();
const querySet = environment.forceFetch.mock.calls[1][0];
expect(Object.keys(querySet)).toEqual(['entity0']);
});
it('resolves data using updated `variables`', () => {
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
const updatedQueryData = [
{__dataID__: '21', id: '21', url: '//www'},
{__dataID__: '42', id: '42', url: '//www'},
];
GraphQLStoreQueryResolver.mockDefaultResolveImplementation(fragment => {
expect(fragment.getVariables()).toEqual({site: 'www'});
return updatedQueryData;
});
environment.primeCache.mock.requests[0].succeed();
expect(mockInstance.state.queryData.entity).toBe(updatedQueryData);
});
it('throws when the queryData is not an array', () => {
const updatedQueryData = {__dataID__: '21', id: '21', url: '//www'};
GraphQLStoreQueryResolver.mockDefaultResolveImplementation(pointer => {
return updatedQueryData;
});
// Change the query data that is stored by the container to
// `updatedQueryData`
mockInstance.forceFetch();
environment.forceFetch.mock.requests[0].succeed();
expect(() => mockInstance.forceFetch()).toFailInvariant(
'RelayContainer: Invalid queryData for `entity`, expected an array ' +
'of records because the corresponding fragment is plural.',
);
});
});
describe('query builders', () => {
it('are called with variables for variables', () => {
expect(entityQuery.mock.calls.length).toBe(1);
expect(entityQuery.mock.calls[0][0].site).toEqual(
QueryBuilder.createCallVariable('site')
);
});
it('are only called once', () => {
expect(entityQuery.mock.calls.length).toBe(1);
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
expect(entityQuery.mock.calls.length).toBe(1);
});
});
describe('mount', () => {
it('renders with default variables', () => {
expect(mockInstance.state.relayProp.variables.site).toBe('mobile');
});
it('renders with default pendingVariables', () => {
expect(mockInstance.state.relayProp.pendingVariables).toBe(null);
});
it('lets props override default variables', () => {
const anotherInstance = RelayTestUtils.createRenderer().render(
genMockPointer => (
<MockContainer entity={genMockPointer('42')} site="www" />
),
environment
);
expect(anotherInstance.state.relayProp.variables.site).toBe('www');
});
});
describe('update', () => {
it('does not update `variables` until data is ready', () => {
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
mockInstance.forceUpdate();
expect(mockInstance.state.relayProp.variables.site).toBe('mobile');
});
it('updates `variables` after callback when data is ready', () => {
const mockCallback = jest.fn();
mockInstance.setVariables({site: 'www'}, mockCallback);
jest.runAllTimers();
mockCallback.mockImplementation(() => {
expect(mockInstance.state.relayProp.variables.site).toBe('mobile');
});
environment.primeCache.mock.requests[0].succeed();
expect(mockCallback.mock.calls.length).toBe(1);
expect(mockInstance.state.relayProp.variables.site).toBe('www');
});
it('resolves data using updated `variables`', () => {
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
const updatedQueryData = {__dataID__: '42', id: '42', url: '//www'};
GraphQLStoreQueryResolver.mockDefaultResolveImplementation(fragment => {
expect(fragment.getVariables()).toEqual({site: 'www'});
return updatedQueryData;
});
environment.primeCache.mock.requests[0].succeed();
expect(mockInstance.state.queryData.entity).toBe(updatedQueryData);
});
it('sets pendingVariables when request is in-flight', () => {
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
environment.primeCache.mock.requests[0].block();
expect(mockInstance.state.relayProp.pendingVariables).toEqual({site: 'www'});
});
it('re-sets pendingVariables when request is aborted', () => {
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
environment.primeCache.mock.requests[0].block();
environment.primeCache.mock.requests[0].abort();
expect(mockInstance.state.relayProp.pendingVariables).toEqual(null);
});
it('re-sets pendingVariables when request succeeded', () => {
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
environment.primeCache.mock.requests[0].block();
environment.primeCache.mock.requests[0].succeed();
expect(mockInstance.state.relayProp.pendingVariables).toEqual(null);
});
it('updates pendingVariables when new request is sent', () => {
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
environment.primeCache.mock.requests[0].block();
mockInstance.setVariables({site: 'test'});
jest.runAllTimers();
environment.primeCache.mock.requests[1].block();
expect(mockInstance.state.relayProp.pendingVariables).toEqual({site: 'test'});
});
it('sets prepared version of variables in pendingVariables', () => {
prepareVariables.mockImplementation((variables, route) => {
return {
...variables,
site: variables.site.toUpperCase(),
};
});
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
environment.primeCache.mock.requests[0].block();
expect(mockInstance.state.relayProp.pendingVariables).toEqual({site: 'WWW'});
});
it('aborts pending requests before creating a new request', () => {
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
environment.primeCache.mock.requests[0].block();
expect(environment.primeCache.mock.abort[0]).not.toBeCalled();
mockInstance.setVariables({site: 'mobile'});
jest.runAllTimers();
expect(environment.primeCache.mock.abort[0]).toBeCalled();
});
it('invokes callback for a request that aborts a pending request', () => {
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
environment.primeCache.mock.requests[0].block();
const mockCallback = jest.fn();
mockInstance.setVariables({site: 'mobile'}, mockCallback);
jest.runAllTimers();
environment.primeCache.mock.requests[1].block();
expect(mockCallback).toBeCalled();
});
it('re-requests the last variables', () => {
mockInstance.setVariables({site: 'mobile'});
jest.runAllTimers();
const {mock} = environment.primeCache;
expect(mock.calls.length).toBe(1);
expect(Object.keys(mock.calls[0][0]).length).toBe(1);
});
it('re-requests currently pending variables', () => {
const requests = environment.primeCache.mock.requests;
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
requests[0].block();
expect(environment.primeCache.mock.abort[0]).not.toBeCalled();
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
requests[0].block();
expect(environment.primeCache.mock.abort[0]).toBeCalled();
expect(environment.primeCache.mock.calls.length).toBe(2);
});
it('re-requests the last variables for `forceFetch`', () => {
mockInstance.forceFetch({site: 'mobile'});
jest.runAllTimers();
const {mock} = environment.forceFetch;
expect(mock.calls.length).toBe(1);
expect(Object.keys(mock.calls[0][0]).length).toBe(1);
});
it('re-requests the last variables with a pending request', () => {
const requests = environment.primeCache.mock.requests;
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
requests[0].block();
const {mock} = environment.primeCache;
expect(mock.abort[0]).not.toBeCalled();
mockInstance.setVariables({site: 'mobile'});
jest.runAllTimers();
requests[0].block();
expect(mock.abort[0]).toBeCalled();
expect(mock.calls.length).toBe(2);
expect(Object.keys(mock.calls[1][0]).length).toBe(1);
});
it('invokes the callback as many times as ready state changes', () => {
const mockFunction = jest.fn(function() {
expect(this.constructor).toBe(MockComponent);
});
mockInstance.setVariables({site: 'www'}, mockFunction);
jest.runAllTimers();
const request = environment.primeCache.mock.requests[0];
request.block();
request.succeed();
expect(mockFunction.mock.calls).toEqual([
[{...defaultState, done: false, ready: false}],
[{...defaultState, done: true, ready: true}],
]);
});
it('invokes the callback with the component as `this`', () => {
const mockFunction = jest.fn(function() {
expect(this.constructor).toBe(MockComponent);
});
mockInstance.setVariables({site: 'www'}, mockFunction);
jest.runAllTimers();
environment.primeCache.mock.requests[0].block();
expect(mockFunction).toBeCalled();
});
it('reconciles only once even if callback calls `setState`', () => {
const before = render.mock.calls.length;
mockInstance.setVariables({site: 'www'}, function() {
this.setState({isLoaded: true});
});
jest.runAllTimers();
environment.primeCache.mock.requests[0].succeed();
expect(render.mock.calls.length - before).toBe(1);
});
it('does not mutate previous `variables`', () => {
const prevVariables = mockInstance.state.relayProp.variables;
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
environment.primeCache.mock.requests[0].succeed();
expect(prevVariables).toEqual({site: 'mobile'});
expect(mockInstance.state.relayProp.variables).not.toBe(prevVariables);
});
it('warns when unknown variable is set', () => {
prepareVariables.mockImplementation(() => {});
mockInstance.setVariables({unknown: 'www'});
expect([
'RelayContainer: Expected query variable `%s` to be initialized in ' +
'`initialVariables`.',
'unknown',
]).toBeWarnedNTimes(1);
});
});
describe('prepareVariables()', () => {
let renderer;
beforeEach(() => {
entityQuery = jest.fn(
() => Relay.QL`fragment on Node{profilePicture(size:$size)}`
);
// Make RQLTransform ignore this call.
MockComponent = React.createClass({render});
const createContainer = Relay.createContainer;
MockContainer = createContainer(MockComponent, {
fragments: {
entity: entityQuery,
},
initialVariables: {
size: 'thumbnail',
prepared: false,
},
prepareVariables,
});
renderer = RelayTestUtils.createRenderer(domContainer);
});
it('calls `prepareVariables` on mount', () => {
prepareVariables.mockImplementation((variables, route) => {
// prepared variables should never be passed back to `prepareVariables`
expect(variables.prepared).toBe(false);
return {
size: 32, // string -> int
prepared: true, // false -> true
};
});
let resolvedVariables = null;
GraphQLStoreQueryResolver.mockDefaultResolveImplementation(resolved => {
resolvedVariables = resolved.getVariables();
});
mockInstance = renderer.render(
genMockPointer =>
<MockContainer entity={genMockPointer('42')} size="medium" />,
environment
);
// prepareVariables output used as props.relay.variables
expect(mockInstance.state.relayProp.variables).toEqual({
size: 32,
prepared: true,
});
// ...and used read fragment data
expect(resolvedVariables).toEqual({
size: 32,
prepared: true,
});
});
it('calls `prepareVariables` in componentWillReceiveProps', () => {
prepareVariables.mockImplementation((variables, route) => {
// prepared variables should never be passed back to `prepareVariables`
expect(variables.prepared).toBe(false);
return {
size: variables.size === 'medium' ? 32 : 64, // string -> int
prepared: true, // false -> true
};
});
mockInstance = renderer.render(
genMockPointer =>
<MockContainer entity={genMockPointer('42')} size="medium" />,
environment
);
// update with new size
let resolvedVariables = null;
GraphQLStoreQueryResolver.mockDefaultResolveImplementation(resolved => {
resolvedVariables = resolved.getVariables();
});
mockInstance = renderer.render(
genMockPointer =>
<MockContainer entity={genMockPointer('42')} size="thumbnail" />,
environment
);
// prepareVariables output used as props.relay.variables
expect(mockInstance.state.relayProp.variables).toEqual({
size: 64,
prepared: true,
});
// ...and used read fragment data
expect(resolvedVariables).toEqual({
size: 64,
prepared: true,
});
});
it('calls `prepareVariables` when `setVariables` is called', () => {
prepareVariables.mockImplementation((variables, route) => {
// prepared variables should never be passed back to `prepareVariables`
expect(variables.prepared).toBe(false);
return {
size: 64, // string -> int
prepared: true, // false -> true
};
});
mockInstance = renderer.render(
genMockPointer => <MockContainer entity={genMockPointer('42')} />,
environment
);
mockInstance.setVariables({size: 'medium'});
const prepareVariablesCalls = prepareVariables.mock.calls;
const calls = prepareVariablesCalls[prepareVariablesCalls.length - 1];
expect(calls[0]).toEqual({
size: 'medium',
prepared: false,
});
expect(calls[1]).toBe(
RelayMetaRoute.get(mockInstance.context.route.name)
);
// `prepareVariables` output is used to prime the cache...
const queries = environment.primeCache.mock.calls[0][0];
const query = queries[Object.keys(queries)[0]];
const fragment = query.getChildren().find(
child => child instanceof RelayQuery.Fragment
);
expect(fragment.getVariables()).toEqual({
size: 64,
prepared: true,
});
let resolvedVariables = null;
GraphQLStoreQueryResolver.mockDefaultResolveImplementation(resolved => {
resolvedVariables = resolved.getVariables();
});
jest.runAllTimers();
environment.primeCache.mock.requests[0].succeed();
// ...and is visible to the component
expect(mockInstance.state.relayProp.variables).toEqual({
size: 64,
prepared: true,
});
// ...and to read fragment data
expect(resolvedVariables).toEqual({
size: 64,
prepared: true,
});
});
it('warns when `prepareVariables` introduces unknown variables', () => {
mockInstance = renderer.render(
genMockPointer => <MockContainer entity={genMockPointer('42')} />,
environment
);
prepareVariables.mockImplementation(
(variables, route) => ({unknown: 0})
);
mockInstance.setVariables({size: 2});
expect([
'RelayContainer: Expected query variable `%s` to be initialized in ' +
'`initialVariables`.',
'unknown',
]).toBeWarnedNTimes(1);
});
});
describe('unmount', () => {
it('aborts pending requests when unmounted', () => {
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
expect(environment.primeCache.mock.abort[0]).not.toBeCalled();
ReactDOM.unmountComponentAtNode(domContainer);
expect(environment.primeCache.mock.abort[0]).toBeCalled();
});
it('ignores `setState` from callback when request aborts', () => {
const mockCallback = jest.fn()
.mockImplementation(readyState => {
if (readyState.mounted) {
this.setState({isAborted: true});
}
});
mockInstance.setVariables({site: 'www'}, mockCallback);
jest.runAllTimers();
expect(mockCallback).not.toBeCalled();
expect(() => {
ReactDOM.unmountComponentAtNode(domContainer);
jest.runAllTimers();
}).not.toThrow();
expect(mockCallback.mock.calls).toEqual([
[{...defaultState, aborted: true, mounted: false}],
]);
});
});
describe('prop variable updates', () => {
it('updates variables if props are updated', () => {
class MockInnerComponent extends React.Component {
render() {
return <div />;
}
}
const MockInnerContainer = Relay.createContainer(MockInnerComponent, {
fragments: {
entity: () => Relay.QL`fragment on Node{url(site:$site)}`,
},
initialVariables: {site: undefined},
});
class MockWrapperComponent extends React.Component {
render() {
return (
<MockInnerContainer
ref="inner"
site={this.props.relay.variables.site}
entity={this.props.entity}
/>
);
}
}
MockContainer = Relay.createContainer(MockWrapperComponent, {
fragments: {
entity: variables => Relay.QL` fragment on Node{
${MockInnerContainer.getFragment('entity', {site: variables.site})}
}`,
},
initialVariables: {site: 'mobile'},
});
mockInstance = RelayTestUtils.createRenderer(domContainer).render(
genMockPointer => <MockContainer entity={genMockPointer('42')} />,
environment
);
const innerComponent = mockInstance.refs.component.refs.inner;
expect(innerComponent.state.relayProp.variables.site).toBe('mobile');
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
environment.primeCache.mock.requests[0].succeed();
expect(mockInstance.state.relayProp.variables.site).toBe('www');
expect(innerComponent.state.relayProp.variables.site).toBe('www');
});
it('resets variables if outside variable props are updated', () => {
class MockInnerComponent extends React.Component {
render() {
return <div />;
}
}
const MockInnerContainer = Relay.createContainer(MockInnerComponent, {
fragments: {
entity: () => Relay.QL` fragment on Actor {
url(site:$site)
profilePicture(size:$size) {
uri
}
}`,
},
initialVariables: {
site: undefined,
size: 48,
},
});
class MockWrapperComponent extends React.Component {
render() {
return (
<MockInnerContainer
ref="inner"
site={this.props.relay.variables.site}
entity={this.props.entity}
/>
);
}
}
MockContainer = Relay.createContainer(MockWrapperComponent, {
fragments: {
entity: variables => Relay.QL` fragment on Actor {
${MockInnerContainer.getFragment('entity', {site: variables.site})}
}`,
},
initialVariables: {site: 'mobile'},
});
mockInstance = RelayTestUtils.createRenderer(domContainer).render(
genMockPointer => <MockContainer entity={genMockPointer('42')} />,
environment
);
const innerComponent = mockInstance.refs.component.refs.inner;
expect(innerComponent.state.relayProp.variables.site).toBe('mobile');
innerComponent.setVariables({size: 32});
jest.runAllTimers();
environment.primeCache.mock.requests[0].succeed();
expect(innerComponent.state.relayProp.variables).toEqual({
site: 'mobile',
size: 32,
});
mockInstance.setVariables({site: 'www'});
jest.runAllTimers();
environment.primeCache.mock.requests[1].succeed();
expect(mockInstance.state.relayProp.variables).toEqual({
site: 'www',
});
expect(innerComponent.state.relayProp.variables).toEqual({
site: 'www',
size: 48,
});
});
it('does not reset variables if outside props are the same', () => {
class MockInnerComponent extends React.Component {
render() {
return <div />;
}
}
const MockInnerContainer = Relay.createContainer(MockInnerComponent, {
fragments: {
entity: () => Relay.QL`fragment on User {
url(site: $site)
storySearch(query: $query) {
id
}
profilePicture(size: $size) {
uri
}
}`,
},
initialVariables: {
site: 'mobile',
query: undefined, // <-- Object type
size: undefined, // <-- Array type
},
});
class MockWrapperComponent extends React.Component {
render() {
return (
<MockInnerContainer
ref="inner"
query={this.props.relay.variables.query}
size={this.props.relay.variables.size}
entity={this.props.entity}
/>
);
}
}
const MockWrapperContainer = Relay.createContainer(MockWrapperComponent, {
fragments: {
entity: variables => Relay.QL`fragment on User {
${MockInnerContainer.getFragment('entity', {
query: variables.query,
size: variables.size,
})}
}`,
},
initialVariables: {
query: { text: 'recent' },
size: [32, 64],
},
});
const mockWrapperInstance = RelayTestUtils.createRenderer(domContainer).render(
genMockPointer => <MockWrapperContainer entity={genMockPointer('42')} />,
environment
);
const innerComponent = mockWrapperInstance.refs.component.refs.inner;
expect(innerComponent.state.relayProp.variables.query).toEqual({
text: 'recent',
});
expect(innerComponent.state.relayProp.variables.size).toEqual([32, 64]);
innerComponent.setVariables({
site: 'www',
});
jest.runAllTimers();
environment.primeCache.mock.requests[0].succeed();
expect(innerComponent.state.relayProp.variables).toEqual({
site: 'www',
query: { text: 'recent' },
size: [32, 64],
});
mockWrapperInstance.setVariables({
query: { text: 'recent' },
size: [32, 64],
});
jest.runAllTimers();
environment.primeCache.mock.requests[1].succeed();
expect(mockWrapperInstance.state.relayProp.variables).toEqual({
query: { text: 'recent' },
size: [32, 64],
});
expect(innerComponent.state.relayProp.variables).toEqual({
site: 'www',
query: { text: 'recent' },
size: [32, 64],
});
});
});
});