src/container/__tests__/RelayReadyStateRenderer-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 React = require('React');
const Relay = require('Relay');
const ReactDOM = require('ReactDOM');
const ReactTestUtils = require('ReactTestUtils');
const RelayEnvironment = require('RelayEnvironment');
const RelayQueryConfig = require('RelayQueryConfig');
const RelayReadyStateRenderer = require('RelayReadyStateRenderer');
const StaticContainer = require('StaticContainer.react');
describe('RelayReadyStateRenderer', () => {
/**
* Creates an asymmetric matcher that passes for values that are container
* props with an optionally constrained data ID and/or fragment.
*
* expect(...).toEqual(anyRecord({
* dataID: '123',
* fragment: Container.getFragment('fragmentName'),
* }));
*
*/
const anyRecord = requirements => {
const expected = {
__dataID__: jasmine.any(String),
__fragments__: jasmine.any(Object),
};
if (requirements.hasOwnProperty('dataID')) {
expected.__dataID__ = requirements.dataID;
}
if (requirements.hasOwnProperty('fragment')) {
const concreteFragmentID = requirements.fragment.getFragment({}).id;
expected.__fragments__ = jasmine.objectContaining({
[concreteFragmentID]: [jasmine.any(Object)],
});
}
return jasmine.objectContaining(expected);
};
/**
* Pretty printer that prints top-level React elements using JSX.
*/
const ppReactElement = element => {
if (!ReactTestUtils.isElement(element) ||
!element.type ||
!element.type.name) {
return jasmine.pp(element);
}
const ppProps = Object.entries(element.props)
.map(([key, value]) => {
const ppValue = jasmine.pp(value);
return ` ${key}={${ppValue.length > 120 ? '...' : ppValue}}`;
})
.join('');
return `<${element.type.name}${ppProps} />`;
};
let defaultProps;
let defaultReadyState;
beforeEach(() => {
jest.resetModuleRegistry();
const TestQueryConfig = RelayQueryConfig.genMock({
routeName: 'TestQueryConfig',
queries: {
node: () => Relay.QL`query { node(id: "123") }`,
},
});
defaultProps = {
Container: Relay.createContainer(() => <div />, {
fragments: {
node: () => Relay.QL`fragment on Node { id }`,
},
}),
environment: new RelayEnvironment(),
queryConfig: new TestQueryConfig(),
retry: jest.fn(),
};
defaultReadyState = {
aborted: false,
done: false,
error: null,
ready: false,
stale: false,
};
});
describe('arguments', () => {
beforeEach(() => {
const container = document.createElement('div');
jasmine.addMatchers({
toRenderWithArgs: () => ({
compare(elementOrReadyState, expected) {
const render = jest.fn(() => <div />);
const element = ReactTestUtils.isElement(elementOrReadyState) ?
React.cloneElement(elementOrReadyState, {render}) :
<RelayReadyStateRenderer
{...defaultProps}
readyState={elementOrReadyState}
render={render}
/>;
ReactDOM.render(element, container);
const actual = render.mock.calls[0][0];
const pass = jasmine.matchersUtil.equals(
actual,
jasmine.objectContaining(expected)
);
return {
get message() {
const not = pass ? ' not' : '';
return (
`Expected ${ppReactElement(elementOrReadyState)}${not} ` +
`to render with arguments ${jasmine.pp(expected)}. ` +
`Instead, it rendered with arguments ${jasmine.pp(actual)}.`
);
},
pass,
};
},
}),
});
});
it('renders without `props` until it is ready', () => {
expect(defaultReadyState).toRenderWithArgs({
props: null,
});
expect({...defaultReadyState, ready: true}).toRenderWithArgs({
props: {node: anyRecord({dataID: '123'})},
});
});
it('renders with a false `done` until it is done', () => {
expect(defaultReadyState).toRenderWithArgs({
done: false,
});
expect({...defaultReadyState, done: true}).toRenderWithArgs({
done: true,
});
});
it('renders with `error` when there is an error', () => {
expect(defaultReadyState).toRenderWithArgs({
error: null,
});
const error = new Error();
expect({...defaultReadyState, error}).toRenderWithArgs({
error,
});
});
it('renders with `props` and `error` when ready with an error', () => {
expect(defaultReadyState).toRenderWithArgs({
error: null,
props: null,
});
const error = new Error();
expect({...defaultReadyState, error, ready: true}).toRenderWithArgs({
error,
props: {node: anyRecord({dataID: '123'})},
});
});
it('renders with `stale` if ready and stale', () => {
expect(defaultReadyState).toRenderWithArgs({
props: null,
stale: false,
});
expect({
...defaultReadyState,
ready: true,
stale: true,
}).toRenderWithArgs({
props: {node: anyRecord({dataID: '123'})},
stale: true,
});
});
it('renders with the supplied `retry` callback', () => {
expect(defaultReadyState).toRenderWithArgs({
retry: defaultProps.retry,
});
});
it('renders with `props` including query config variables', () => {
const AnotherQueryConfig = RelayQueryConfig.genMock();
const anotherQueryConfig = new AnotherQueryConfig({
foo: 123,
bar: 456,
});
expect(
<RelayReadyStateRenderer
{...defaultProps}
queryConfig={anotherQueryConfig}
readyState={{...defaultReadyState, ready: true}}
/>
).toRenderWithArgs({
props: jasmine.objectContaining({
foo: 123,
bar: 456,
}),
});
});
it('updates `props` when the query config changes', () => {
expect(
<RelayReadyStateRenderer
{...defaultProps}
readyState={{...defaultReadyState, ready: true}}
/>
).toRenderWithArgs({
props: {node: anyRecord({dataID: '123'})},
});
const AnotherQueryConfig = RelayQueryConfig.genMock({
routeName: 'AnotherQueryConfig',
queries: {
node: () => Relay.QL`query { node(id: "456") }`,
},
});
expect(
<RelayReadyStateRenderer
{...defaultProps}
queryConfig={new AnotherQueryConfig()}
readyState={{...defaultReadyState, ready: true}}
/>
).toRenderWithArgs({
props: {node: anyRecord({dataID: '456'})},
});
});
it('updates `props` when the query results change', () => {
const AnotherQueryConfig = RelayQueryConfig.genMock({
routeName: 'AnotherQueryConfig',
queries: {
me: () => Relay.QL`query { me }`,
},
});
const anotherQueryConfig = new AnotherQueryConfig();
const environment = new RelayEnvironment();
defaultProps = {
Container: Relay.createContainer(() => <div />, {
fragments: {
me: () => Relay.QL`fragment on User { id }`,
},
}),
environment,
queryConfig: anotherQueryConfig,
retry: jest.fn(),
};
environment.getStoreData().getRecordWriter().putDataID('me', null, '123');
expect(
<RelayReadyStateRenderer
{...defaultProps}
queryConfig={anotherQueryConfig}
readyState={{...defaultReadyState, ready: true}}
/>
).toRenderWithArgs({
props: {me: anyRecord({dataID: '123'})},
});
environment.getStoreData().getRecordWriter().putDataID('me', null, '456');
expect(
<RelayReadyStateRenderer
{...defaultProps}
queryConfig={anotherQueryConfig}
readyState={{...defaultReadyState, ready: true}}
/>
).toRenderWithArgs({
props: {me: anyRecord({dataID: '456'})},
});
});
it('updates `props` when the query results become non-null', () => {
const AnotherQueryConfig = RelayQueryConfig.genMock({
routeName: 'AnotherQueryConfig',
queries: {
me: () => Relay.QL`query { me }`,
},
});
const anotherQueryConfig = new AnotherQueryConfig();
const environment = new RelayEnvironment();
defaultProps = {
Container: Relay.createContainer(() => <div />, {
fragments: {
me: () => Relay.QL`fragment on User { id }`,
},
}),
environment,
queryConfig: anotherQueryConfig,
retry: jest.fn(),
};
environment.getStoreData().getRecordWriter().putDataID('me', null, null);
expect(
<RelayReadyStateRenderer
{...defaultProps}
queryConfig={anotherQueryConfig}
readyState={{...defaultReadyState, ready: true}}
/>
).toRenderWithArgs({
props: {me: null},
});
environment.getStoreData().getRecordWriter().putDataID('me', null, '123');
expect(
<RelayReadyStateRenderer
{...defaultProps}
queryConfig={anotherQueryConfig}
readyState={{...defaultReadyState, ready: true}}
/>
).toRenderWithArgs({
props: {me: anyRecord({dataID: '123'})},
});
});
it('updates `props` when the container changes', () => {
expect(
<RelayReadyStateRenderer
{...defaultProps}
readyState={{...defaultReadyState, ready: true}}
/>
).toRenderWithArgs({
props: {
node: anyRecord({
dataID: '123',
fragment: defaultProps.Container.getFragment('node'),
}),
},
});
const AnotherContainer = Relay.createContainer(() => <div />, {
fragments: {
node: () => Relay.QL`fragment on Node { id }`,
},
});
expect(
<RelayReadyStateRenderer
{...defaultProps}
Container={AnotherContainer}
readyState={{...defaultReadyState, ready: true}}
/>
).toRenderWithArgs({
props: {
node: anyRecord({
dataID: '123',
fragment: AnotherContainer.getFragment('node'),
}),
},
});
});
it('updates `props` when the environment changes', () => {
// Declare a query that requires a lookup in the root call map.
const AnotherQueryConfig = RelayQueryConfig.genMock({
routeName: 'AnotherQueryConfig',
queries: {
node: () => Relay.QL`query { me }`,
},
});
defaultProps.environment.getStoreData()
.getRecordWriter()
.putDataID('me', null, '123');
expect(
<RelayReadyStateRenderer
{...defaultProps}
queryConfig={new AnotherQueryConfig()}
readyState={{...defaultReadyState, ready: true}}
/>
).toRenderWithArgs({
props: {
node: anyRecord({dataID: '123'}),
},
});
const anotherEnvironment = new RelayEnvironment();
anotherEnvironment.getStoreData()
.getRecordWriter()
.putDataID('me', null, '456');
expect(
<RelayReadyStateRenderer
{...defaultProps}
environment={anotherEnvironment}
queryConfig={new AnotherQueryConfig()}
readyState={{...defaultReadyState, ready: true}}
/>
).toRenderWithArgs({
props: {
node: anyRecord({dataID: '456'}),
},
});
});
});
describe('children', () => {
beforeEach(() => {
const container = document.createElement('div');
function render(element) {
return ReactTestUtils.findRenderedComponentWithType(
ReactDOM.render(element, container),
StaticContainer
);
}
jasmine.addMatchers({
toRenderChild: () => ({
compare(element, expected) {
const pass = jasmine.matchersUtil.equals(
render(element).props.children,
expected
);
return {
get message() {
const not = pass ? ' not' : '';
return (
`Expected ${ppReactElement(element)}${not} ` +
`to render child ${jasmine.pp(expected)}.`
);
},
pass,
};
},
}),
toUpdateChild: () => ({
compare(element) {
const pass = render(element).props.shouldUpdate;
return {
get message() {
const not = pass ? ' not' : '';
return (
`Expected ${ppReactElement(element)}${(not)} ` +
'to update child.'
);
},
pass,
};
},
}),
});
});
it('renders null if `readyState` is initially omitted', () => {
expect(
<RelayReadyStateRenderer
{...defaultProps}
render={() => <div />}
/>
).toRenderChild(null);
});
it('renders null if not ready and `render` is initially omitted', () => {
expect(
<RelayReadyStateRenderer
{...defaultProps}
readyState={defaultReadyState}
/>
).toRenderChild(null);
});
it('renders component if ready and `render` is omitted', () => {
expect(
<RelayReadyStateRenderer
{...defaultProps}
readyState={{...defaultReadyState, ready: true}}
/>
).toRenderChild(
jasmine.objectContaining({type: defaultProps.Container})
);
});
it('renders null if `render` returns null', () => {
expect(
<RelayReadyStateRenderer
{...defaultProps}
readyState={defaultReadyState}
render={() => null}
/>
).toRenderChild(null);
});
it('does not update child if `render` returns undefined', () => {
const prevChild = <span />;
expect(
<RelayReadyStateRenderer
{...defaultProps}
readyState={defaultReadyState}
render={() => prevChild}
/>
).toRenderChild(prevChild);
expect(
<RelayReadyStateRenderer
{...defaultProps}
readyState={defaultReadyState}
render={() => undefined}
/>
).not.toUpdateChild();
});
it('updates child if `render` returns a new view', () => {
const prevChild = <span />;
expect(
<RelayReadyStateRenderer
{...defaultProps}
readyState={defaultReadyState}
render={() => prevChild}
/>
).toRenderChild(prevChild);
const nextChild = <div />;
expect(
<RelayReadyStateRenderer
{...defaultProps}
readyState={defaultReadyState}
render={() => nextChild}
/>
).toUpdateChild(nextChild);
});
});
describe('context', () => {
it('sets environment and query config on the React context', () => {
class TestComponent extends React.Component {
static contextTypes = {
relay: Relay.PropTypes.Environment,
route: Relay.PropTypes.QueryConfig.isRequired,
};
render() {
this.props.onRenderContext(this.context);
return null;
}
}
const onRenderContext = jest.fn();
const container = document.createElement('div');
ReactDOM.render(
<RelayReadyStateRenderer
{...defaultProps}
readyState={defaultReadyState}
render={() => <TestComponent onRenderContext={onRenderContext} />}
/>,
container
);
expect(onRenderContext).toBeCalledWith({
relay: defaultProps.environment,
route: defaultProps.queryConfig,
});
});
});
});