import * as React from 'react'; import { shallow, mount } from 'enzyme'; import { MockConnectHost } from './MockConnectHost'; import { MockConnectIframeProvider } from './MockConnectIframeProvider'; import { MockIframeLoadingIndicator } from './MockIframeLoadingIndicator'; import { MockIframeTimeoutIndicator } from './MockIframeTimeoutIndicator'; import { MockIframeFailedToLoadIndicator } from './MockIframeFailedToLoadIndicator'; import { ConnectIframe, ConnectIframeDefinitions, LoadingState } from '../ConnectIframe'; import { analytics } from '../../../adaptors/analytics/AnalyticsAdaptor'; import IFrameLifecycleEventManager from '../IFrameLifecycleEventManager'; import { AnalyticsCategories, AnalyticsActions } from '../../../adaptors/analytics/AnalyticsConstants'; function flushPromises() { return new Promise(resolve => setImmediate(resolve)); } jest.mock('../IFrameLifecycleEventManager', () => { return require('./MockIframeLifecycleEventManager'); }); jest.mock('../../../adaptors/analytics/AnalyticsAdaptor', () => { const { mockAnalytics } = require('./MockAnalyticsAdaptor'); return { analytics: mockAnalytics }; }); const props:ConnectIframeDefinitions.Props = { appKey: 'appKey', moduleKey: 'moduleKey', url: 'url', width: 'width', height: 'height', options: {}, connectHost: new MockConnectHost(), connectIframeProvider: new MockConnectIframeProvider() }; describe('ConnectIframe', () => { beforeEach(() => { analytics.trigger.mockClear(); props.connectHost.createExtension.mockClear(); props.connectHost.destroy.mockClear(); props.connectIframeProvider.onHideInlineDialog.mockClear(); props.connectIframeProvider.onStoreNestedIframeJSON.mockClear(); props.connectIframeProvider.buildIframeStyles.mockClear(); }); describe('#resize', () => { it('renders correct height and width', async() => { const wrapper:ConnectIframe = mount() as ConnectIframe; await flushPromises(); wrapper.instance().resize('newWidth', 'newHeight'); wrapper.update(); expect(wrapper.find('div').props().style.width).toBe('auto'); expect(wrapper.find('div').props().style.height).toBe('auto'); expect(wrapper.find('iframe').props().width).toBe('newWidth'); expect(wrapper.find('iframe').props().height).toBe('newHeight'); }); }); describe('#sizeToParent', () => { it('renders correct height and width', async() => { const wrapper:ConnectIframe = mount() as ConnectIframe; await flushPromises(); wrapper.instance().sizeToParent(); wrapper.update(); expect(wrapper.find('div').props().style.width).toBe('100%'); expect(wrapper.find('div').props().style.height).toBe('100%'); expect(wrapper.find('iframe').props().width).toBe('100%'); expect(wrapper.find('iframe').props().height).toBe('100%'); }); }); describe('#iframeContainer', () => { it('renders iframe without div container', async() => { const wrapper:ConnectIframe = shallow( children}/>) as ConnectIframe; await flushPromises(); expect(wrapper.find('div').length).toBe(0); expect(wrapper.find('iframe').length).toBe(1); }); }); describe('#isSubIframe', () => { describe('in a non-nested iframe', () => { it('returns false', () => { const wrapper:ConnectIframe = shallow() as ConnectIframe; expect(wrapper.instance()._isSubIframe()).toBe(false); }); }); describe('in a nested iframe', () => { it('returns true', () => { const wrapper:ConnectIframe = shallow() as ConnectIframe; wrapper.setState({hostFrameOffset: 2}); expect(wrapper.instance()._isSubIframe()).toBe(true); }); }); }); describe('#hideInlineDialog', () => { it('does not cause error if not in an inline dialog', () => { const wrapper:ConnectIframe = shallow() as ConnectIframe; wrapper.instance().hideInlineDialog(); expect(props.connectIframeProvider.onHideInlineDialog).not.toHaveBeenCalled(); }); it('invokes the provider method if registered', () => { const wrapper:ConnectIframe = shallow() as ConnectIframe; wrapper.instance().hideInlineDialog(); expect(props.connectIframeProvider.onHideInlineDialog).toHaveBeenCalledTimes(1); }); }); describe('#render', () => { it('renders a provided timeout indicator', async() => { const wrapper:ConnectIframe = shallow() as ConnectIframe; await flushPromises(); wrapper.instance().iframeTimeoutCallback(); wrapper.update(); expect(wrapper.html()).toEqual(expect.stringContaining(MockIframeTimeoutIndicator.getHtml())); }); it('renders a provided failed to load indicator', async() => { const wrapper:ConnectIframe = shallow() as ConnectIframe; await flushPromises(); wrapper.instance().iframeFailedToLoadCallback(); wrapper.update(); expect(wrapper.html()).toEqual(expect.stringContaining(MockIframeFailedToLoadIndicator.getHtml())); }); describe('Loading indicator', () => { let wrapper:ConnectIframe; beforeEach(() => { wrapper = shallow() as ConnectIframe; }); it('displays a provided loading indicator while loading', async() => { await flushPromises(); wrapper.update(); expect(wrapper.html()).toEqual(expect.stringContaining(MockIframeLoadingIndicator.getHtml()); }); it('does not display a provided loading indicator once loaded', async() => { await flushPromises(); wrapper.instance().iframeEstablishedCallback(); wrapper.update(); expect(wrapper.html()).not.toEqual(expect.stringContaining(MockIframeLoadingIndicator.getHtml()); }); }); it('Hides the iframe while loading', async() => { const wrapper = shallow(); await flushPromises(); wrapper.update(); expect(JSON.stringify(wrapper.find('iframe').props().style)).toEqual(JSON.stringify({opacity: 0})); }); it('Shows the iframe once loaded', async() => { const wrapper = shallow(); await flushPromises(); wrapper.instance().iframeEstablishedCallback(); wrapper.update(); expect(JSON.stringify(wrapper.find('iframe').props().style)).toEqual(JSON.stringify({})); }); it('Using product-provided iframe styles', () => { const wrapper = shallow(); wrapper.instance().componentDidMount(); expect(props.connectIframeProvider.buildIframeStyles).toHaveBeenCalledTimes(1); expect(JSON.stringify(wrapper.find('iframe').props().style)).toEqual(JSON.stringify({key: 'value'})); }); it('uses the iframe atttributes provided by simple-xdm', async() => { const wrapper = shallow(); await flushPromises(); wrapper.update(); expect(wrapper.find('iframe').props().id).toEqual('mock-iframe-id'); expect(wrapper.find('iframe').props().src).toEqual(props.url); expect(wrapper.find('iframe').props().name).toEqual('mock-iframe-name'); }); }); describe('#componentWillUnmount', () => { let wrapper:ConnectIframe; beforeEach(() => { wrapper = shallow() as ConnectIframe; }); it('invokes all registered callbacks', () => { const cb1 = jest.fn(); const cb2 = jest.fn(); wrapper.instance().registerUnmountCallback(cb1); wrapper.instance().registerUnmountCallback(cb2); wrapper.instance().componentWillUnmount(); expect(cb1).toHaveBeenCalledTimes(1); expect(cb2).toHaveBeenCalledTimes(1); }); }); describe('#componentDidMount', () => { let wrapper:ConnectIframe; describe('in a non-nested iframe', () => { beforeEach(() => { wrapper = shallow() as ConnectIframe; }); it('it does not store nested iframe JSON', async() => { await flushPromises(); wrapper.update(); expect(props.connectIframeProvider.onStoreNestedIframeJSON).not.toHaveBeenCalled(); }); it('builds an xdm bridge', async() => { await flushPromises(); wrapper.update(); expect(props.connectHost.createExtension).toHaveBeenCalledTimes(1); }); it('triggers an analytics event', async() => { await flushPromises(); wrapper.update(); expect(analytics.trigger).toHaveBeenCalledTimes(1); expect(analytics.trigger).toHaveBeenCalledWith( AnalyticsCategories.iframe, AnalyticsActions.create, 'appKey', expect.anything() ); }); }); describe('in a nested iframe', () => { beforeEach(() => { const nestedProps = Object.assign({}, props, {hostFrameOffset: 2}); wrapper = shallow() as ConnectIframe; }); it('stores the nested iframe JSON', async() => { await flushPromises(); wrapper.update(); expect(props.connectIframeProvider.onStoreNestedIframeJSON).toHaveBeenCalledTimes(1); }); }); }); describe('#componentDidUpdate', () => { let wrapper:ConnectIframe; describe('in a non-nested iframe', () => { beforeEach(() => { wrapper = shallow() as ConnectIframe; }); it('it does not store nested iframe JSON', async() => { await flushPromises(); wrapper.update(); expect(props.connectIframeProvider.onStoreNestedIframeJSON).not.toHaveBeenCalled(); }); it('builds an xdm bridge', async() => { await flushPromises(); wrapper.update(); expect(props.connectHost.createExtension).toHaveBeenCalledTimes(1); }); it('triggers an analytics event', async() => { await flushPromises(); wrapper.update(); expect(analytics.trigger).toHaveBeenCalledTimes(1); expect(analytics.trigger).toHaveBeenCalledWith( AnalyticsCategories.iframe, AnalyticsActions.create, 'appKey', expect.anything() ); }); }); describe('in a nested iframe', () => { beforeEach(() => { const nestedProps = Object.assign({}, props, {hostFrameOffset: 2}); wrapper = shallow() as ConnectIframe; }); it('stores the nested iframe JSON', async() => { await flushPromises(); wrapper.update(); expect(props.connectIframeProvider.onStoreNestedIframeJSON).toHaveBeenCalledTimes(1); }); }); }); it('resets on appKey change', async() => { const wrapper:ConnectIframe = mount() as ConnectIframe; await flushPromises(); wrapper.update(); const firstIFrameLifecycleEventManager:IFrameLifecycleEventManager = wrapper.instance().getIFrameLifecycleEventManager(); wrapper.setProps(Object.assign({}, props, {appKey: props.appKey+'1'})); await flushPromises(); wrapper.update(); const secondIFrameLifecycleEventManager:IFrameLifecycleEventManager = wrapper.instance().getIFrameLifecycleEventManager(); expect(props.connectHost.destroy).toHaveBeenCalledTimes(1); expect(props.connectHost.createExtension).toHaveBeenCalledTimes(2); expect(firstIFrameLifecycleEventManager.unregister).toHaveBeenCalledTimes(1); expect(secondIFrameLifecycleEventManager.unregister).not.toHaveBeenCalled(); }); }); it('resets on moduleKey change', async() => { const wrapper:ConnectIframe = mount() as ConnectIframe; await flushPromises(); wrapper.update(); const firstIFrameLifecycleEventManager:IFrameLifecycleEventManager = wrapper.instance().getIFrameLifecycleEventManager(); wrapper.setProps(Object.assign({}, props, {moduleKey: props.moduleKey+'1'})); await flushPromises(); wrapper.update(); const secondIFrameLifecycleEventManager:IFrameLifecycleEventManager = wrapper.instance().getIFrameLifecycleEventManager(); expect(props.connectHost.destroy).toHaveBeenCalledTimes(1); expect(props.connectHost.createExtension).toHaveBeenCalledTimes(2); expect(firstIFrameLifecycleEventManager.unregister).toHaveBeenCalledTimes(1); expect(secondIFrameLifecycleEventManager.unregister).not.toHaveBeenCalled(); }); it('does not reset on width change', async() => { const wrapper:ConnectIframe = mount() as ConnectIframe; await flushPromises(); wrapper.update(); const firstIFrameLifecycleEventManager:IFrameLifecycleEventManager = wrapper.instance().getIFrameLifecycleEventManager(); wrapper.setProps(Object.assign({}, props, {width: props.width+'1'})); expect(props.connectHost.destroy).not.toHaveBeenCalled(); expect(props.connectHost.createExtension).toHaveBeenCalledTimes(1); }); it('does not reset on height change', async() => { const wrapper:ConnectIframe = mount() as ConnectIframe; await flushPromises(); wrapper.update(); const firstIFrameLifecycleEventManager:IFrameLifecycleEventManager = wrapper.instance().getIFrameLifecycleEventManager(); wrapper.setProps(Object.assign({}, props, {height: props.height+'1'})); expect(props.connectHost.destroy).not.toHaveBeenCalled(); expect(props.connectHost.createExtension).toHaveBeenCalledTimes(1); }); it('creates only a single xdm bridge', async() => { const wrapper:ConnectIframe = mount() as ConnectIframe; await flushPromises(); wrapper.update(); expect(props.connectHost.createExtension).toHaveBeenCalledTimes(1); }); describe('iframe context resolver', () => { it('runs if required', async() => { let customProps = Object.assign({}, props, {connectIframeProvider: new MockConnectIframeProvider()}); customProps.connectIframeProvider.resolveIframeContext = jest.fn(iframeContext => new Promise(resolve => resolve(Object.assign({}, iframeContext, {url: 'resolved-url'})))); const wrapper:ConnectIframe = mount() as ConnectIframe; await flushPromises(); wrapper.update(); expect(customProps.connectIframeProvider.resolveIframeContext).toHaveBeenCalledTimes(1); expect(wrapper.find('iframe').props().src).toEqual('resolved-url'); }); it('does not break the component if not registered', async() => { let customProps = Object.assign({}, props, {connectIframeProvider: new MockConnectIframeProvider()}); customProps.connectIframeProvider.resolveIframeContext = undefined; const wrapper:ConnectIframe = mount() as ConnectIframe; await flushPromises(); wrapper.update(); expect(wrapper.find('iframe').props().src).toEqual(customProps.url); }); }); });