Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert WithTracking to function component #142

Merged
merged 11 commits into from
Oct 18, 2019
77 changes: 77 additions & 0 deletions src/__tests__/e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React, { useContext } from 'react';
import { mount } from 'enzyme';
import PropTypes from 'prop-types';
import hoistNonReactStatics from 'hoist-non-react-statics';

const dispatchTrackingEvent = jest.fn();
jest.setMock('../dispatchTrackingEvent', dispatchTrackingEvent);
Expand Down Expand Up @@ -633,6 +635,81 @@ describe('e2e', () => {
expect(innerRenderCount).toEqual(1);
});

it('does not prevent components using the legacy context API and hoist-non-react-statics < v3.1.0 from receiving updates', () => {
const withLegacyContext = DecoratedComponent => {
class WithLegacyContext extends React.Component {
static contextTypes = { theme: PropTypes.string };

render() {
return (
<DecoratedComponent
{...this.props} // eslint-disable-line react/jsx-props-no-spreading
theme={this.context.theme}
/>
);
}
}

hoistNonReactStatics(WithLegacyContext, DecoratedComponent);

// Explicitly hoist statc contextType to simulate behavior of
// hoist-non-react-statics versions older than v3.1.0
WithLegacyContext.contextType = DecoratedComponent.contextType;

return WithLegacyContext;
};

@track()
class Top extends React.Component {
render() {
return this.props.children;
}
}

@withLegacyContext
@track({ page: 'Page' }, { dispatchOnMount: true })
class Page extends React.Component {
render() {
return <span>{this.props.theme}</span>;
}
}

@track()
class App extends React.Component {
static childContextTypes = { theme: PropTypes.string };

constructor(props) {
super(props);
this.state = { theme: 'light' };
}

getChildContext() {
return { theme: this.state.theme };
}

handleUpdateTheme = () => {
this.setState({ theme: 'dark' });
};

render() {
return (
<div>
<button type="button" onClick={this.handleUpdateTheme} />
<Top>
<Page />
</Top>
</div>
);
}
}

const wrapper = mount(<App />);
expect(wrapper.find('span').text()).toBe('light');

wrapper.find('button').simulate('click');
expect(wrapper.find('span').text()).toBe('dark');
});

it('root context items are accessible to children', () => {
const {
ReactTrackingContext,
Expand Down
178 changes: 95 additions & 83 deletions src/__tests__/withTrackingComponentDecorator.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable react/destructuring-assignment,react/prop-types,react/jsx-props-no-spreading */
import React from 'react';
import { shallow } from 'enzyme';
import { shallow, mount } from 'enzyme';

const mockDispatchTrackingEvent = jest.fn();
jest.setMock('../dispatchTrackingEvent', mockDispatchTrackingEvent);
Expand All @@ -16,133 +17,129 @@ describe('withTrackingComponentDecorator', () => {
expect(typeof decorated).toBe('function');
});

describe('with a function trackingContext', () => {
describe('with process option', () => {
const props = { props: 1 };
const context = { context: 1 };
const trackingContext = jest.fn(() => {});
const trackingContext = { page: 1 };
const process = jest.fn(() => null);

@withTrackingComponentDecorator({}, { process })
class ParentTestComponent extends React.Component {
static displayName = 'ParentTestComponent';

render() {
return this.props.children;
}
}

@withTrackingComponentDecorator(trackingContext)
class TestComponent {
class TestComponent extends React.Component {
static displayName = 'TestComponent';
}

const myTC = new TestComponent(props, context);
render() {
return null;
}
}

beforeEach(() => {
mockDispatchTrackingEvent.mockClear();
});

it('defines the expected static properties', () => {
expect(TestComponent.displayName).toBe('WithTracking(TestComponent)');
expect(TestComponent.contextType).toBeDefined();
});

it('calls trackingContext() in getContextForProvider', () => {
expect(myTC.contextValueForProvider.tracking.getTrackingData()).toEqual(
{}
it('process function gets called', () => {
mount(
<ParentTestComponent {...props}>
<TestComponent />
</ParentTestComponent>
);
expect(trackingContext).toHaveBeenCalledTimes(1);
});

it('dispatches event in trackEvent', () => {
const data = { data: 1 };
myTC.trackEvent({ data });
expect(mockDispatchTrackingEvent).toHaveBeenCalledWith({ data });
});

it('does not dispatch event in componentDidMount', () => {
myTC.componentDidMount();
expect(process).toHaveBeenCalled();
expect(mockDispatchTrackingEvent).not.toHaveBeenCalled();
});

it('renders', () => {
expect(myTC.render()).toBeDefined();
});
});

describe('with an object trackingContext', () => {
describe('with process option from parent and dispatchOnMount option on component', () => {
const props = { props: 1 };
const context = { context: 1 };
const trackingContext = { page: 1 };
const process = jest.fn(() => ({ event: 'pageView' }));
const dispatchOnMount = jest.fn(() => ({ specificEvent: true }));

@withTrackingComponentDecorator(trackingContext, {
dispatchOnMount: true,
})
class TestComponent {
static displayName = 'TestComponent';
}

const myTC = new TestComponent(props, context);

beforeEach(() => {
mockDispatchTrackingEvent.mockClear();
});

// We'll only test what differs from the functional trackingContext variation

it('returns the proper object in getContextForProvider', () => {
expect(Object.keys(myTC.contextValueForProvider.tracking)).toEqual([
'dispatch',
'getTrackingData',
'process',
]);
});

it('dispatches event in componentDidMount', () => {
myTC.componentDidMount();
expect(mockDispatchTrackingEvent).toHaveBeenCalledWith(trackingContext);
});
});
@withTrackingComponentDecorator({}, { process })
class ParentTestComponent extends React.Component {
static displayName = 'ParentTestComponent';

describe('with process option', () => {
const props = { props: 1 };
const trackingContext = { page: 1 };
const process = jest.fn(() => ({ event: 'pageView' }));
const context = { context: 1, tracking: { process } };
render() {
return this.props.children;
}
}

@withTrackingComponentDecorator(trackingContext)
class TestComponent {
@withTrackingComponentDecorator(trackingContext, { dispatchOnMount })
class TestComponent extends React.Component {
static displayName = 'TestComponent';
}

const myTC = new TestComponent(props, context);
render() {
return null;
}
}

beforeEach(() => {
mockDispatchTrackingEvent.mockClear();
});

it('process function gets called', () => {
myTC.componentDidMount();
it('dispatches only once when process and dispatchOnMount functions are passed', () => {
mount(
<ParentTestComponent {...props}>
<TestComponent />
</ParentTestComponent>
);

expect(process).toHaveBeenCalled();
expect(dispatchOnMount).toHaveBeenCalled();
expect(mockDispatchTrackingEvent).toHaveBeenCalledWith({
page: 1,
event: 'pageView',
specificEvent: true,
});
expect(mockDispatchTrackingEvent).toHaveBeenCalledTimes(1);
});
});

describe('with process option from parent and dispatchOnMount option on component', () => {
const props = { props: 1 };
const trackingContext = { page: 1 };
describe('with function trackingContext', () => {
const props = { page: 1 };
const trackingContext = jest.fn(p => ({ page: p.page }));
const process = jest.fn(() => ({ event: 'pageView' }));
const context = { context: 1, tracking: { process } };
const dispatchOnMount = jest.fn(() => ({ specificEvent: true }));

@withTrackingComponentDecorator({}, { process })
class ParentTestComponent extends React.Component {
static displayName = 'ParentTestComponent';

render() {
return this.props.children;
}
}

@withTrackingComponentDecorator(trackingContext, { dispatchOnMount })
class TestComponent {
class TestComponent extends React.Component {
static displayName = 'TestComponent';
}

const myTC = new TestComponent(props, context);
render() {
return null;
}
}

beforeEach(() => {
mockDispatchTrackingEvent.mockClear();
});

it('dispatches only once when process and dispatchOnMount functions are passed', () => {
myTC.componentDidMount();
mount(
<ParentTestComponent>
<TestComponent {...props} />
</ParentTestComponent>
);

expect(process).toHaveBeenCalled();
expect(dispatchOnMount).toHaveBeenCalled();
expect(trackingContext).toHaveBeenCalled();
expect(mockDispatchTrackingEvent).toHaveBeenCalledWith({
page: 1,
event: 'pageView',
Expand All @@ -156,22 +153,37 @@ describe('withTrackingComponentDecorator', () => {
const props = { props: 1 };
const trackingContext = { page: 1 };
const process = jest.fn(() => null);
const context = { context: 1, tracking: { process } };
const dispatchOnMount = true;

@withTrackingComponentDecorator({}, { process })
class ParentTestComponent extends React.Component {
static displayName = 'ParentTestComponent';

render() {
return this.props.children;
}
}

@withTrackingComponentDecorator(trackingContext, { dispatchOnMount })
class TestComponent {
class TestComponent extends React.Component {
static displayName = 'TestComponent';
}

const myTC = new TestComponent(props, context);
render() {
return null;
}
}

beforeEach(() => {
mockDispatchTrackingEvent.mockClear();
});

it('dispatches only once when process and dispatchOnMount functions are passed', () => {
myTC.componentDidMount();
mount(
<ParentTestComponent {...props}>
<TestComponent />
</ParentTestComponent>
);

expect(process).toHaveBeenCalled();
expect(mockDispatchTrackingEvent).toHaveBeenCalledWith({
page: 1,
Expand Down
Loading