diff --git a/test/core/workflow/workflow-acrobat/action-binder.test.js b/test/core/workflow/workflow-acrobat/action-binder.test.js index 42580a774..457dbf89b 100644 --- a/test/core/workflow/workflow-acrobat/action-binder.test.js +++ b/test/core/workflow/workflow-acrobat/action-binder.test.js @@ -3037,4 +3037,122 @@ describe('ActionBinder', () => { expect(Array.isArray(result)).to.be.true; }); }); + + describe('ensurePageConfig', () => { + let originalFetch; + + const mockFetchResponse = (config = {}) => ({ + ok: true, + json: async () => ({ location: 'https://test-location.com', config }), + }); + + const mockSatellite = (content = null) => { + window._satellite = { + track: (event, options) => { + if (typeof options.done === 'function') { + const result = content + ? { decisions: [{ items: [{ data: { content } }] }], propositions: [] } + : { decisions: [], propositions: [] }; + setTimeout(() => options.done(result, null), 0); + } + }, + }; + }; + + beforeEach(() => { + originalFetch = window.fetch; + window.unityConfig.pageConfigEndPoint = 'https://test-api.adobe.com/pageconfig'; + actionBinder.pageConfigFetched = false; + actionBinder.pageConfigPromise = null; + actionBinder.pageConfigLocation = null; + actionBinder.experimentData = null; + actionBinder.experimentViaPageConfig = false; + sinon.stub(actionBinder, 'dispatchErrorToast').resolves(); + }); + + afterEach(() => { + window.fetch = originalFetch; + delete window._satellite; + }); + + it('should not run again when pageConfigFetched is already true', async () => { + actionBinder.pageConfigFetched = true; + window.fetch = sinon.stub(); + + await actionBinder.ensurePageConfig(); + + expect(window.fetch.called).to.be.false; + }); + + it('should reset pageConfigFetched and pageConfigPromise when getExperimentData fails on first attempt without dispatching error', async () => { + window.fetch = sinon.stub().resolves(mockFetchResponse({ target: { enabled: true, decisionScopes: ['scope1'] } })); + window._satellite = { + track: (event, options) => { + if (typeof options.done === 'function') setTimeout(() => options.done(null, new Error('Target error')), 0); + }, + }; + + await actionBinder.ensurePageConfig(); + + expect(actionBinder.pageConfigFetched).to.be.false; + expect(actionBinder.pageConfigPromise).to.be.null; + expect(actionBinder.dispatchErrorToast.called).to.be.false; + }); + + it('should reset flags and dispatch error to Splunk when getExperimentData fails on retry', async () => { + actionBinder.pageConfigLocation = 'https://existing-location.com'; + window.fetch = sinon.stub().resolves(mockFetchResponse({ target: { enabled: true, decisionScopes: ['scope1'] } })); + window._satellite = { + track: (event, options) => { + if (typeof options.done === 'function') setTimeout(() => options.done(null, new Error('Target error')), 0); + }, + }; + + await actionBinder.ensurePageConfig(); + + expect(actionBinder.pageConfigFetched).to.be.false; + expect(actionBinder.pageConfigPromise).to.be.null; + expect(actionBinder.dispatchErrorToast.calledOnce).to.be.true; + }); + + it('should set experimentData and experimentViaPageConfig when pageConfig target is enabled', async () => { + const mockData = { variationId: 'variant-1' }; + window.fetch = sinon.stub().resolves(mockFetchResponse({ target: { enabled: true, decisionScopes: ['scope1'] } })); + mockSatellite(mockData); + + await actionBinder.ensurePageConfig(); + + expect(actionBinder.experimentData).to.deep.equal(mockData); + expect(actionBinder.experimentViaPageConfig).to.be.true; + expect(actionBinder.pageConfigFetched).to.be.true; + }); + + it('should set experimentData via targetCfg.experimentationOn when pageConfig target is not enabled', async () => { + const mockData = { variationId: 'variant-2' }; + actionBinder.workflowCfg.enabledFeatures = ['compress-pdf']; + actionBinder.workflowCfg.targetCfg.experimentationOn = ['compress-pdf']; + window.fetch = sinon.stub() + .onFirstCall().resolves(mockFetchResponse({})) + .onSecondCall() + .resolves({ ok: true, json: async () => ({ country: 'US' }) }); + mockSatellite(mockData); + + await actionBinder.ensurePageConfig(); + + expect(actionBinder.experimentData).to.deep.equal(mockData); + expect(actionBinder.experimentViaPageConfig).to.be.false; + expect(actionBinder.pageConfigFetched).to.be.true; + }); + + it('should not call _satellite when pageConfig target is not enabled and verb not in experimentationOn', async () => { + window.fetch = sinon.stub().resolves(mockFetchResponse({})); + window._satellite = { track: sinon.stub() }; + + await actionBinder.ensurePageConfig(); + + expect(window._satellite.track.called).to.be.false; + expect(actionBinder.experimentData).to.be.null; + expect(actionBinder.pageConfigFetched).to.be.true; + }); + }); }); diff --git a/test/utils/experiment-provider.test.js b/test/utils/experiment-provider.test.js index 25f1378f2..12ce9b4dd 100644 --- a/test/utils/experiment-provider.test.js +++ b/test/utils/experiment-provider.test.js @@ -1,5 +1,6 @@ /* eslint-disable no-underscore-dangle */ import { expect } from '@esm-bundle/chai'; +import sinon from 'sinon'; import getExperimentData, { getDecisionScopesForVerb } from '../../unitylibs/utils/experiment-provider.js'; describe('getExperimentData', () => { @@ -118,6 +119,40 @@ describe('getExperimentData', () => { }); }); +describe('waitForSatellite behavior in getExperimentData', () => { + afterEach(() => { + delete window._satellite; + }); + + it('should wait for _satellite to become available and then succeed', async () => { + delete window._satellite; + setTimeout(() => { + window._satellite = { + track: (event, options) => { + if (typeof options.done === 'function') setTimeout(() => options.done(null, null), 0); + }, + }; + }, 50); + const result = await getExperimentData(['acom_unity_acrobat_compress-pdf']); + expect(result).to.equal(null); + }); + + it('should reject when _satellite is not available within timeout', async () => { + delete window._satellite; + const clock = sinon.useFakeTimers(); + try { + const promise = getExperimentData(['acom_unity_acrobat_compress-pdf']); + await clock.tickAsync(5100); + await promise; + expect.fail('Should have rejected'); + } catch (error) { + expect(error.message).to.equal('_satellite not available within timeout'); + } finally { + clock.restore(); + } + }); +}); + describe('getDecisionScopesForVerb', () => { let originalFetch; diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index aba5413f5..fde6f29ec 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -273,10 +273,12 @@ export default class ActionBinder { if (this.pageConfigFetched) return; this.pageConfigFetched = true; const verb = this.workflowCfg.enabledFeatures[0]; + const isRetry = !!this.pageConfigLocation; + let pageConfig; try { const { fetchPageConfig } = await import('../../../scripts/utils.js'); const { default: getExperimentData } = await import('../../../utils/experiment-provider.js'); - const pageConfig = await fetchPageConfig({ product: 'acrobat', verb }); + pageConfig = await fetchPageConfig({ product: 'acrobat', verb }); this.pageConfigLocation = pageConfig.location; if (pageConfig.config?.target?.enabled) { this.experimentData = await getExperimentData(pageConfig.config.target.decisionScopes); @@ -287,10 +289,22 @@ export default class ActionBinder { this.experimentData = await getExperimentData(decisionScopes); } } catch (error) { - await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { - code: 'warn_fetch_experiment', - desc: error.message, - }); + if (!pageConfig) { + await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { + code: 'warn_fetch_experiment', + desc: error.message, + }); + this.acrobatApiConfig = this.getAcrobatApiConfig(); + return; + } + if (isRetry) { + await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { + code: 'warn_fetch_experiment', + desc: error.message, + }); + } + this.pageConfigFetched = false; + this.pageConfigPromise = null; } this.acrobatApiConfig = this.getAcrobatApiConfig(); } diff --git a/unitylibs/utils/experiment-provider.js b/unitylibs/utils/experiment-provider.js index f102ba724..62e2477de 100644 --- a/unitylibs/utils/experiment-provider.js +++ b/unitylibs/utils/experiment-provider.js @@ -14,11 +14,29 @@ export async function getDecisionScopesForVerb(verb) { return region ? [`${verbScope}_${region}`, verbScope] : [verbScope]; } +function waitForSatellite(timeout = 5000) { + if (window._satellite) return Promise.resolve(); + return new Promise((resolve, reject) => { + const start = Date.now(); + const interval = setInterval(() => { + if (window._satellite) { + clearInterval(interval); + resolve(); + } else if (Date.now() - start >= timeout) { + clearInterval(interval); + reject(new Error('_satellite not available within timeout')); + } + }, 100); + }); +} + export default async function getExperimentData(decisionScopes) { if (!decisionScopes || decisionScopes.length === 0) { throw new Error('No decision scopes provided for experiment data fetch'); } + await waitForSatellite(); + return new Promise((resolve, reject) => { try { window._satellite.track('propositionFetch', {