From a74aa88334c53b326def0e8b3075a8f095205f7a Mon Sep 17 00:00:00 2001 From: Ruchika Sinha Date: Wed, 24 Jun 2026 08:13:48 -0700 Subject: [PATCH 1/6] wait for satellite --- unitylibs/utils/experiment-provider.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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', { From e0cc669b5505ae33fbe7df0730e24cbfe892cbe0 Mon Sep 17 00:00:00 2001 From: Ruchika Sinha Date: Wed, 24 Jun 2026 09:36:51 -0700 Subject: [PATCH 2/6] optimize --- .../workflow/workflow-acrobat/action-binder.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index aba5413f5..16cd97e3e 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -194,6 +194,7 @@ export default class ActionBinder { this.initialize(); this.experimentData = null; this.experimentViaPageConfig = false; + this.experimentDataPromise = null; this.pageConfigLocation = null; this.pageConfigFetched = false; this.pageConfigPromise = null; @@ -275,16 +276,18 @@ export default class ActionBinder { const verb = this.workflowCfg.enabledFeatures[0]; try { const { fetchPageConfig } = await import('../../../scripts/utils.js'); - const { default: getExperimentData } = await import('../../../utils/experiment-provider.js'); + const { default: getExperimentData, getDecisionScopesForVerb } = await import('../../../utils/experiment-provider.js'); const pageConfig = await fetchPageConfig({ product: 'acrobat', verb }); this.pageConfigLocation = pageConfig.location; if (pageConfig.config?.target?.enabled) { - this.experimentData = await getExperimentData(pageConfig.config.target.decisionScopes); - this.experimentViaPageConfig = true; + this.experimentDataPromise = getExperimentData(pageConfig.config.target.decisionScopes) + .then((data) => { this.experimentData = data; this.experimentViaPageConfig = true; }) + .catch((error) => this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { code: 'warn_fetch_experiment', desc: error.message })); } else if (!this.experimentData && this.workflowCfg.targetCfg?.experimentationOn?.includes(verb)) { - const { getDecisionScopesForVerb } = await import('../../../utils/experiment-provider.js'); - const decisionScopes = await getDecisionScopesForVerb(verb); - this.experimentData = await getExperimentData(decisionScopes); + this.experimentDataPromise = getDecisionScopesForVerb(verb) + .then((decisionScopes) => getExperimentData(decisionScopes)) + .then((data) => { this.experimentData = data; }) + .catch((error) => this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { code: 'warn_fetch_experiment', desc: error.message })); } } catch (error) { await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { @@ -541,6 +544,7 @@ export default class ActionBinder { if (this.multiFileValidationFailure) cOpts.payload.feedback = 'uploaderror'; if (this.showInfoToast) cOpts.payload.feedback = 'nonpdf'; } + if (this.experimentDataPromise) await this.experimentDataPromise; if (this.experimentData && (this.experimentViaPageConfig || this.workflowCfg.targetCfg?.experimentationOn?.includes(this.workflowCfg.enabledFeatures[0]))) { cOpts.payload.variationId = this.experimentData.variationId; } From 35aab6c52b8827c5183a9f6880227b11b62680d1 Mon Sep 17 00:00:00 2001 From: Ruchika Sinha Date: Wed, 24 Jun 2026 09:51:20 -0700 Subject: [PATCH 3/6] optimize --- .../workflow/workflow-acrobat/action-binder.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index 16cd97e3e..aba5413f5 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -194,7 +194,6 @@ export default class ActionBinder { this.initialize(); this.experimentData = null; this.experimentViaPageConfig = false; - this.experimentDataPromise = null; this.pageConfigLocation = null; this.pageConfigFetched = false; this.pageConfigPromise = null; @@ -276,18 +275,16 @@ export default class ActionBinder { const verb = this.workflowCfg.enabledFeatures[0]; try { const { fetchPageConfig } = await import('../../../scripts/utils.js'); - const { default: getExperimentData, getDecisionScopesForVerb } = await import('../../../utils/experiment-provider.js'); + const { default: getExperimentData } = await import('../../../utils/experiment-provider.js'); const pageConfig = await fetchPageConfig({ product: 'acrobat', verb }); this.pageConfigLocation = pageConfig.location; if (pageConfig.config?.target?.enabled) { - this.experimentDataPromise = getExperimentData(pageConfig.config.target.decisionScopes) - .then((data) => { this.experimentData = data; this.experimentViaPageConfig = true; }) - .catch((error) => this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { code: 'warn_fetch_experiment', desc: error.message })); + this.experimentData = await getExperimentData(pageConfig.config.target.decisionScopes); + this.experimentViaPageConfig = true; } else if (!this.experimentData && this.workflowCfg.targetCfg?.experimentationOn?.includes(verb)) { - this.experimentDataPromise = getDecisionScopesForVerb(verb) - .then((decisionScopes) => getExperimentData(decisionScopes)) - .then((data) => { this.experimentData = data; }) - .catch((error) => this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { code: 'warn_fetch_experiment', desc: error.message })); + const { getDecisionScopesForVerb } = await import('../../../utils/experiment-provider.js'); + const decisionScopes = await getDecisionScopesForVerb(verb); + this.experimentData = await getExperimentData(decisionScopes); } } catch (error) { await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { @@ -544,7 +541,6 @@ export default class ActionBinder { if (this.multiFileValidationFailure) cOpts.payload.feedback = 'uploaderror'; if (this.showInfoToast) cOpts.payload.feedback = 'nonpdf'; } - if (this.experimentDataPromise) await this.experimentDataPromise; if (this.experimentData && (this.experimentViaPageConfig || this.workflowCfg.targetCfg?.experimentationOn?.includes(this.workflowCfg.enabledFeatures[0]))) { cOpts.payload.variationId = this.experimentData.variationId; } From 17cf0164f544cdaf7661c552a0d45f1db10e34aa Mon Sep 17 00:00:00 2001 From: Ruchika Sinha Date: Wed, 24 Jun 2026 12:24:27 -0700 Subject: [PATCH 4/6] unitylibs/core/workflow/workflow-acrobat/action-binder.js --- .../workflow/workflow-acrobat/action-binder.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index aba5413f5..a3b5b3421 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -273,11 +273,21 @@ export default class ActionBinder { if (this.pageConfigFetched) return; this.pageConfigFetched = true; const verb = this.workflowCfg.enabledFeatures[0]; + 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; + } catch (error) { + await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { + code: 'warn_fetch_experiment', + desc: error.message, + }); + this.acrobatApiConfig = this.getAcrobatApiConfig(); + return; + } + try { + const { default: getExperimentData } = await import('../../../utils/experiment-provider.js'); if (pageConfig.config?.target?.enabled) { this.experimentData = await getExperimentData(pageConfig.config.target.decisionScopes); this.experimentViaPageConfig = true; @@ -291,6 +301,8 @@ export default class ActionBinder { code: 'warn_fetch_experiment', desc: error.message, }); + this.pageConfigFetched = false; + this.pageConfigPromise = null; } this.acrobatApiConfig = this.getAcrobatApiConfig(); } From 2086d1538acfcd8bf5bcd8c3b948c4fa11ddace3 Mon Sep 17 00:00:00 2001 From: Ruchika Sinha Date: Wed, 24 Jun 2026 20:52:37 -0700 Subject: [PATCH 5/6] wip --- .../workflow-acrobat/action-binder.js | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index a3b5b3421..fde6f29ec 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -273,21 +273,13 @@ 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'); pageConfig = await fetchPageConfig({ product: 'acrobat', verb }); this.pageConfigLocation = pageConfig.location; - } catch (error) { - await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { - code: 'warn_fetch_experiment', - desc: error.message, - }); - this.acrobatApiConfig = this.getAcrobatApiConfig(); - return; - } - try { - const { default: getExperimentData } = await import('../../../utils/experiment-provider.js'); if (pageConfig.config?.target?.enabled) { this.experimentData = await getExperimentData(pageConfig.config.target.decisionScopes); this.experimentViaPageConfig = true; @@ -297,10 +289,20 @@ 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; } From 25dc828784a71ef4e8969f65c3b14d965c32935d Mon Sep 17 00:00:00 2001 From: Ruchika Sinha Date: Thu, 25 Jun 2026 14:24:44 -0700 Subject: [PATCH 6/6] add unit tests --- .../workflow-acrobat/action-binder.test.js | 118 ++++++++++++++++++ test/utils/experiment-provider.test.js | 35 ++++++ 2 files changed, 153 insertions(+) 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;