UNPKG

salsify-experiences-sdk

Version:

SDK to be used by commerce websites to implement product experiences.

380 lines (330 loc) 16.2 kB
/** * @jest-environment jsdom */ import EnhancedContentApi from '../index' import { PerProductConfigCache, selectSource } from '../perProductConfig' import { makeContext, makeSettings, makeResponse } from '../../__tests__/helpers' import commonSuite from './index.common' import request from '../../utils/request' jest.mock('../../utils/request') jest.mock('../perProductConfig') const clientId = 'client-id' const languageCode = 'lang-code' const enhancedContent = {} const productWithCdnRoot = 'https://testenv.test.salsify.com/brand-space-cdn/sdk/client-id/lang-code/BTF/id-type/existing-product/index.html' const productWithEcPath = `https://salsify-ecdn.com/sdk/${clientId}/${languageCode}/BTF/id-type/existing-product` let ecApi: EnhancedContentApi const log = jest.fn() const settings = makeSettings({ clientId, languageCode, enhancedContent, tracking: true }) const settingsNoTrack = makeSettings({ clientId, languageCode, enhancedContent, tracking: false }) const settingsCdnRoot = makeSettings({ clientId, languageCode, enhancedContent, tracking: true, cdnRoot: 'https://testenv.test.salsify.com/brand-space-cdn', }) commonSuite('browser') const PerProductConfigCacheMock = PerProductConfigCache as jest.Mock const selectSourceMock = selectSource as jest.Mock const headMock = request.head as jest.Mock const exampleContent = '<div>enhanced-content</div>' const emptyContent = '' describe('EnhancedContentApi (browser)', () => { beforeEach(() => { ecApi = new EnhancedContentApi(settings, makeContext(), { log }) PerProductConfigCacheMock.mock.instances[0].getConfig.mockImplementation(() => undefined) }) afterEach(() => { jest.clearAllMocks() }) describe('renderIframe', () => { beforeEach(() => { headMock.mockImplementation(() => makeResponse(exampleContent)) }) // This must all be one test because the iframeResizeListener can only be attached once test('iframe resize listener', async () => { let eventListenerCallback!: EventListener window.addEventListener = jest.fn((_type, callback) => (eventListenerCallback = callback as EventListener)) const container = document.createElement('div') await ecApi.renderIframe(container, 'foo', 'id-type') await ecApi.renderIframe(container, 'foo', 'id-type') // iframe resize listener is attached once and only once expect(window.addEventListener).toHaveBeenCalledTimes(1) eventListenerCallback( new MessageEvent('message', { origin: 'https://salsify-ecdn.com', data: { messageType: 'heightUpdateRequest', height: 100 }, }) ) // iframe is not found because container hasn't been appended to document yet expect(log).toBeCalledTimes(3) // two ec_render_iframe + one error expect(log).toHaveBeenNthCalledWith(3, 'error', { errorContext: 'iframeResizeListener', errorType: 'dom', errorMessage: 'Could not find iframe with selector #salsify-ec-iframe', }) document.querySelector('body')?.append(container) const iframe = document.querySelector('#salsify-ec-iframe') as HTMLIFrameElement expect(iframe.height).toBe('0') eventListenerCallback( new MessageEvent('message', { origin: 'https://salsify-ecdn.com', data: { messageType: 'heightUpdateRequest', height: 100 }, }) ) // iframe is found after appending container, so no additional error is logged expect(log).toBeCalledTimes(3) expect(iframe.height).toBe('100') }) test('should throw error when no product ID type is specified when calling', async () => { const container = document.createElement('div') const check = (): Promise<void> | undefined => ecApi.renderIframe(container, 'product') return expect(check).rejects.toThrowError('No ID type specified.') }) test('it renders the iframe with the correct src', async () => { const container = document.createElement('div') await ecApi.renderIframe(container, 'existing-product', 'id-type') expect(container.firstElementChild?.getAttribute('src')).toBe(`${productWithEcPath}/index.html`) expect(PerProductConfigCacheMock.mock.instances.length).toBe(1) expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledTimes(1) expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledWith(productWithEcPath) }) test('it renders the iframe with the cdn root url', async () => { ecApi = new EnhancedContentApi(settingsCdnRoot, makeContext(), { log }) const container = document.createElement('div') await ecApi.renderIframe(container, 'existing-product', 'id-type') expect(container.firstElementChild?.getAttribute('src')).toBe(productWithCdnRoot) }) test('it sends an ec_render_iframe event', async () => { PerProductConfigCacheMock.mock.instances[0].getConfig.mockResolvedValue(undefined) const container = document.createElement('div') await ecApi.renderIframe(container, 'existing-product', 'id-type') expect(headMock).toHaveBeenCalledTimes(1) expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/index.html`) const renderConfig = { idType: 'id-type', productId: 'existing-product', content: null, allContentExists: false, source: 'index.html', sourceExists: true, } expect(ecApi.lastRenderConfig).toMatchObject(renderConfig) expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig) }) describe('when EC is missing', () => { beforeEach(() => { headMock.mockImplementation(() => makeResponse(emptyContent)) }) test('it sends an ec_render_iframe event with sourceExists: false', async () => { PerProductConfigCacheMock.mock.instances[0].getConfig.mockResolvedValue(undefined) const container = document.createElement('div') await ecApi.renderIframe(container, 'existing-product', 'id-type') expect(headMock).toHaveBeenCalledTimes(1) expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/index.html`) const renderConfig = { idType: 'id-type', productId: 'existing-product', content: null, allContentExists: false, source: 'index.html', sourceExists: false, } expect(ecApi.lastRenderConfig).toMatchObject(renderConfig) expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig) }) }) describe('with config content', () => { const content = [ { source: 'foo.html', weight: 0.4 }, { source: 'bar.html', weight: 0.4 }, { source: null, weight: 0.2 }, ] beforeEach(() => { PerProductConfigCacheMock.mock.instances[0].getConfig.mockImplementation(() => ({ content, })) }) test('it renders the iframe with the selected src', async () => { selectSourceMock.mockImplementation(() => content[1]) const container = document.createElement('div') await ecApi.renderIframe(container, 'existing-product', 'id-type') expect(container.firstElementChild?.getAttribute('src')).toBe(`${productWithEcPath}/bar.html`) expect(PerProductConfigCacheMock.mock.instances.length).toBe(1) expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledTimes(1) expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledWith(productWithEcPath) expect(headMock).toHaveBeenCalledTimes(2) expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/foo.html`) expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/bar.html`) const renderConfig = { idType: 'id-type', productId: 'existing-product', content, allContentExists: true, source: 'bar.html', sourceExists: true, } expect(ecApi.lastRenderConfig).toMatchObject(renderConfig) expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig) }) test('it does not render an iframe when a null source is selected', async () => { selectSourceMock.mockImplementation(() => content[2]) const container = document.createElement('div') await ecApi.renderIframe(container, 'existing-product', 'id-type') expect(container.firstElementChild).toBeNull() expect(PerProductConfigCacheMock.mock.instances.length).toBe(1) expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledTimes(1) expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledWith(productWithEcPath) expect(headMock).toHaveBeenCalledTimes(2) expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/foo.html`) expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/bar.html`) const renderConfig = { idType: 'id-type', productId: 'existing-product', content, allContentExists: true, source: null, sourceExists: false, } expect(ecApi.lastRenderConfig).toMatchObject(renderConfig) expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig) }) describe('when tracking is disabled', () => { beforeEach(() => { ecApi = new EnhancedContentApi(settingsNoTrack, makeContext(), { log }) PerProductConfigCacheMock.mock.instances[1].getConfig.mockImplementation(() => ({ content, })) }) test('it falls back to default source', async () => { selectSourceMock.mockImplementation(() => content[1]) const container = document.createElement('div') await ecApi.renderIframe(container, 'existing-product', 'id-type') expect(container.firstElementChild?.getAttribute('src')).toBe(`${productWithEcPath}/index.html`) expect(PerProductConfigCacheMock.mock.instances.length).toBe(2) expect(PerProductConfigCacheMock.mock.instances[1].getConfig).toHaveBeenCalledTimes(1) expect(PerProductConfigCacheMock.mock.instances[1].getConfig).toHaveBeenCalledWith(productWithEcPath) expect(headMock).toHaveBeenCalledTimes(3) expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/foo.html`) expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/bar.html`) expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/index.html`) const renderConfig = { idType: 'id-type', productId: 'existing-product', content, allContentExists: true, source: 'index.html', sourceExists: true, } expect(ecApi.lastRenderConfig).toMatchObject(renderConfig) expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig) }) }) describe('when all content is missing', () => { beforeEach(() => { headMock.mockImplementation(() => makeResponse(emptyContent)) }) test('it reports allContentExists: false, sourceExists: false', async () => { selectSourceMock.mockImplementation(() => content[1]) const container = document.createElement('div') await ecApi.renderIframe(container, 'existing-product', 'id-type') expect(container.firstElementChild?.getAttribute('src')).toBe(`${productWithEcPath}/bar.html`) expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledTimes(1) expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledWith(productWithEcPath) expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/foo.html`) expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/bar.html`) expect(headMock).toHaveBeenCalledTimes(2) const renderConfig = { idType: 'id-type', productId: 'existing-product', content, allContentExists: false, source: 'bar.html', sourceExists: false, } expect(ecApi.lastRenderConfig).toMatchObject(renderConfig) expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig) }) }) describe('when some content is missing, but selected source is not', () => { beforeEach(() => { headMock.mockImplementation((url: string) => makeResponse(url.match(/\/bar\.html$/) ? emptyContent : exampleContent) ) }) test('it reports allContentExists: false, sourceExists: true', async () => { selectSourceMock.mockImplementation(() => content[0]) const container = document.createElement('div') await ecApi.renderIframe(container, 'existing-product', 'id-type') expect(container.firstElementChild?.getAttribute('src')).toBe(`${productWithEcPath}/foo.html`) expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledTimes(1) expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledWith(productWithEcPath) expect(headMock).toHaveBeenCalledTimes(2) const renderConfig = { idType: 'id-type', productId: 'existing-product', content, allContentExists: false, source: 'foo.html', sourceExists: true, } expect(ecApi.lastRenderConfig).toMatchObject(renderConfig) expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig) }) }) }) describe('with only null config content', () => { const content = [{ source: null, weight: 1 }] beforeEach(() => { PerProductConfigCacheMock.mock.instances[0].getConfig.mockImplementation(() => ({ content, })) }) test('it does not render an iframe and reports allContentExists: false, sourceExists: false', async () => { selectSourceMock.mockImplementation(() => content[0]) const container = document.createElement('div') await ecApi.renderIframe(container, 'existing-product', 'id-type') expect(container.firstElementChild).toBeNull() expect(PerProductConfigCacheMock.mock.instances.length).toBe(1) expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledTimes(1) expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledWith(productWithEcPath) expect(headMock).toHaveBeenCalledTimes(0) const renderConfig = { idType: 'id-type', productId: 'existing-product', content, allContentExists: false, source: null, sourceExists: false, } expect(ecApi.lastRenderConfig).toMatchObject(renderConfig) expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig) }) }) describe('beforeRender', () => { let beforeRender: () => void let ecApi: EnhancedContentApi beforeEach(() => { beforeRender = jest.fn() ecApi = new EnhancedContentApi(settings, makeContext(), { log: jest.fn() }, { beforeRender }) }) test('calls beforeRender when set', async () => { const container = document.createElement('div') await ecApi.renderIframe(container, 'product', 'foo') expect(beforeRender).toHaveBeenCalledTimes(1) }) }) }) describe('updateLanguageCode', () => { test('it updates the language code used for subsequent requests', async () => { const newLanguageCode = 'new-lang-code' ecApi.updateLanguageCode(newLanguageCode) const container = document.createElement('div') await ecApi.renderIframe(container, 'existing-product', 'id-type') const productWithNewLanguageCode = `${productWithEcPath}/index.html`.replace('lang-code', newLanguageCode) expect(container.firstElementChild?.getAttribute('src')).toBe(productWithNewLanguageCode) }) }) })