react-oc
Version:
A React component that allows OpenComponents to operate within a react application.
345 lines (296 loc) • 14.9 kB
JavaScript
import 'jest-plugin-console-matchers/setup';
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOMServer from 'react-dom/server';
import Promise from 'bluebird';
import jQuery from 'jquery';
import { renderAsync, hydrateAsync } from './__test__/react-helpers'
import { OpenComponentsContext } from "./OpenComponentsContext";
import { OCContext } from "./OCContext";
import { OpenComponent } from "./OpenComponent";
describe('<OpenComponent />', () => {
describe('When not mounted within a <ComponentContext />', () => {
it('throws an error', () => {
const node = document.createElement('div');
return expect(renderAsync(<OpenComponent name='my-component' />, node)).rejects
.toThrow(/OpenComponent must be nested within a <ComponentContext \/>/);
});
});
const oc = {};
const fakeResponse = '<oc-component src="http://localhost/my-component"></oc-component>';
const baseContext = {
oc,
baseUrl: 'http://localhost/',
getElements: () => {},
getHtml: () => {},
saveElements: () => {},
};
beforeEach(() => {
oc.build = jest.fn().mockImplementation(() => fakeResponse);
oc.$ = jQuery;
oc.renderNestedComponent = jest.fn((_, cb) => cb());
});
it('throws when no name is provided', () => {
const node = document.createElement('div');
return expect(renderAsync(
<OCContext.Provider value={{...baseContext}}>
<OpenComponent />
</OCContext.Provider>, node)).rejects
.toThrow(/Mandatory prop 'name' is missing./);
});
it('throws when no baseUrl is provided in context', () => {
const node = document.createElement('div');
const {baseUrl, ...rest} = baseContext;
return expect(renderAsync(
<OCContext.Provider value={{...rest}}>
<OpenComponent name='my-component' />
</OCContext.Provider>, node)).rejects
.toThrow(/<OpenComponentsContext> must have a defined 'baseUrl' prop to use this component/);
});
it('should apply the given id to a container div', async () => {
const node = document.createElement('div');
await renderAsync(
<OCContext.Provider value={{...baseContext}}>
<OpenComponent id='my-unique-id' name='my-component' />
</OCContext.Provider>, node);
expect(node.childNodes[0].id).toBe('my-unique-id');
});
it('should apply the given className to a container div', async () => {
const node = document.createElement('div');
await renderAsync(
<OCContext.Provider value={{...baseContext}}>
<OpenComponent className='my-class' name='my-component' />
</OCContext.Provider>, node);
expect(node.childNodes[0].className).toBe('my-class');
});
it('should call oc.build with relevant parameters', async () => {
const node = document.createElement('div');
const parameters = {
hello: 'world'
};
await renderAsync(
<OCContext.Provider value={{...baseContext, oc, lang: 'en-GB'}}>
<OpenComponent name='my-component' version='1.X.X'
parameters={parameters}/>
</OCContext.Provider>,
node);
expect(oc.build).toBeCalledWith({
name: 'my-component',
version: '1.X.X',
baseUrl: 'http://localhost/',
lang: 'en-GB',
parameters,
});
});
it('should call oc.build with lang from component over context', async () => {
const node = document.createElement('div');
await renderAsync(
<OCContext.Provider value={{...baseContext, oc, lang: 'en-GB'}}>
<OpenComponent name='my-component' lang='en-US'/>
</OCContext.Provider>,
node);
expect(oc.build).toBeCalledWith(expect.objectContaining({
lang: 'en-US',
}));
});
it('should render the response of oc.build', async () => {
const node = document.createElement('div');
const parameters = {
hello: 'world'
};
await renderAsync(
<OCContext.Provider value={{...baseContext, oc}}>
<OpenComponent name='my-component' version='1.X.X'
lang='en-GB' parameters={parameters}/>
</OCContext.Provider>,
node);
expect(node.innerHTML).toContain(fakeResponse);
});
it('should call oc.renderNestedComponent with a jquery element containing the response of oc.build', async () => {
const node = document.createElement('div');
const parameters = {
hello: 'world'
};
await renderAsync(
<OCContext.Provider value={{...baseContext, oc}}>
<OpenComponent name='my-component' version='1.X.X'
lang='en-GB' parameters={parameters} />
</OCContext.Provider>, node);
expect(oc.renderNestedComponent).toBeCalledWith(expect.objectContaining({
0: expect.objectContaining({
outerHTML: expect.stringContaining(fakeResponse)
})}),
expect.anything() //callback function
);
});
describe('when a given a captureAs prop', () => {
it('calls saveElements on context with the captureAs value and oc-component element after oc finishes rendering', async () => {
const node = document.createElement('div');
const saveElements = jest.fn()
const getElements = jest.fn();
const fakeResponse = '<oc-component src="http://localhost/my-component"></oc-component>';
oc.build.mockImplementation(() => fakeResponse);
await renderAsync(
<OCContext.Provider value={{...baseContext, saveElements, getElements, oc}}>
<OpenComponent name='my-component' captureAs='my-component-1' />
</OCContext.Provider>, node);
expect(saveElements).toBeCalledWith('my-component-1', [expect.objectContaining({
outerHTML: fakeResponse
})]);
});
it('does not allow dangerouslySetInnerHtml remove existing markup', async () => {
/**
* This test protects a bug fix from regression.
*
* Context: React may call the render function at any time, and in some cases, without
* using the lifecycle methods (shouldComponentUpdate and componentDidUpdate).
* This combined with the dangerouslySetInnerHtml property, if react detects that the
* render method returned anything different, it may choose to update the browser DOM.
*
* This test triggers the behaviour described and ensures that even though oc will modify
* the originally specified markup, the render method does not cause React to undo this.
*/
const node = document.createElement('div');
let elements;
const saveElements = jest.fn((key, els) => elements = els);
const getElements = jest.fn(() => elements);
const fakeResponse = '<oc-component src="http://localhost/my-component"></oc-component>';
const modifiedFakeResponse = '<oc-component src="http://localhost/my-component">hello world</oc-component>'
oc.build.mockImplementation(() => fakeResponse);
oc.renderNestedComponent.mockImplementation((component, cb) => {
component[0].innerHTML = 'hello world';
cb();
});
class RenderTwice extends React.Component {
render() {
this.state = {};
setTimeout(() => {
this.setState({renderAgain: true});
}, 3);
return (
<OCContext.Provider value={{...baseContext, saveElements, getElements, oc}}>
{this.state.renderAgain}
<OpenComponent name='my-component' captureAs='my-component-1' />
</OCContext.Provider>
);
}
}
await renderAsync(<RenderTwice />, node);
await Promise.delay(5);
expect(node.innerHTML).toContain(modifiedFakeResponse);
})
describe('when calling context.getElements with the captureAs prop returns a html element', () => {
it('calls getElements with the captureAs key', async () => {
const node = document.createElement('div');
const element = document.createElement('span');
const getElements = jest.fn().mockImplementation((key) => [element]);
await renderAsync(
<OCContext.Provider value={{...baseContext, getElements, oc}}>
<OpenComponent name='my-component' captureAs='my-component-1' />
</OCContext.Provider>, node);
expect(getElements).toBeCalledWith('my-component-1');
});
it('injects the elements into the container', async () => {
const node = document.createElement('div');
const prevNode = document.createElement('span');
prevNode.innerHTML = '<span>span</span>hello<div>div</div>';
const elements = [...prevNode.childNodes];
const getElements = jest.fn().mockImplementation((key) => elements)
await renderAsync(
<OCContext.Provider value={{...baseContext, getElements, oc}}>
<OpenComponent name='my-component' captureAs='my-component-1' />
</OCContext.Provider>, node);
const div = node.childNodes[0];
expect(div.childNodes.length).toBe(elements.length);
for(var i = 0; i < elements.length; i++) {
// using toBe ensures that it is the same element with
// all event handlers etc. in-tact.
expect(div.childNodes[i]).toBe(elements[i]);
}
});
});
});
it('should throw an error when context does not have an oc property', () => {
const node = document.createElement('div');
const {oc, ...rest} = baseContext;
return expect(renderAsync(
<OCContext.Provider value={{...rest}}>
<OpenComponent name='header' />
</OCContext.Provider>, node)).rejects
.toThrow(/clientOc not defined/);
});
describe('universal support', () => {
it('should not throw when no baseUrl is provided in context when server side rendering', () => {
const node = document.createElement('div');
const {baseUrl, ...rest} = baseContext;
return expect(() => ReactDOMServer.renderToString(
<OCContext.Provider value={{...rest}}>
<OpenComponent name='my-component' />
</OCContext.Provider>))
.not.toThrow();
});
it('should not throw an error when context does not have an oc property but using server side rendering', () => {
const {oc, ...rest} = baseContext;
const app = (
<OCContext.Provider value={{...rest}}>
<OpenComponent name='header' />
</OCContext.Provider>
);
expect(() => ReactDOMServer.renderToString(app)).not.toThrow();
});
it('should render an empty div when server side rendering without oc', () => {
const node = document.createElement('div');
node.innerHTML = ReactDOMServer.renderToString(
<OCContext.Provider value={{...baseContext, oc: undefined }}>
<OpenComponent name='my-component' />
</OCContext.Provider>);
expect(node.innerHTML).toBe('<div></div>');
});
it('should change the empty div markup after hydrating over server rendered markup', async () => {
const node = document.createElement('div');
node.innerHTML = ReactDOMServer.renderToString(
<OCContext.Provider value={{...baseContext, oc: undefined }}>
<OpenComponent name='my-component' />
</OCContext.Provider>);
const existingMarkup = node.innerHTML;
await hydrateAsync(
<OCContext.Provider value={{...baseContext}}>
<OpenComponent name='my-component' />
</OCContext.Provider>, node);
expect(node.innerHTML).not.toBe(existingMarkup);
});
it('should call oc.build with the correct parameters after hydrating', async () => {
const node = document.createElement('div');
node.innerHTML = ReactDOMServer.renderToString(
<OCContext.Provider value={{...baseContext, oc: undefined, lang: 'en-GB' }}>
<OpenComponent name='my-component' />
</OCContext.Provider>);
await hydrateAsync(
<OCContext.Provider value={{...baseContext, oc, lang: 'en-GB'}}>
<OpenComponent name='my-component' />
</OCContext.Provider>, node);
expect(oc.build).toHaveBeenLastCalledWith({
baseUrl: baseContext.baseUrl,
lang: 'en-GB',
name: 'my-component',
});
});
it('should call oc.renderNestedComponent with a jquery element containing the response of oc.build after hydrating', async () => {
const node = document.createElement('div');
node.innerHTML = ReactDOMServer.renderToString(
<OCContext.Provider value={{...baseContext, oc: undefined }}>
<OpenComponent name='my-component' />
</OCContext.Provider>);
await hydrateAsync(
<OCContext.Provider value={{...baseContext}}>
<OpenComponent name='my-component' />
</OCContext.Provider>, node);
expect(oc.renderNestedComponent).toBeCalledWith(expect.objectContaining({
0: expect.objectContaining({
outerHTML: expect.stringContaining(fakeResponse)
})}),
expect.anything() //callback function
);
});
});
});