From 3443a4d5ac35ad68e39e93fe90bee2e3f0301391 Mon Sep 17 00:00:00 2001 From: vipulg Date: Mon, 15 Dec 2025 00:24:26 +0530 Subject: [PATCH 1/4] MWPW-183524 restructure to use common NetworkUtils in workflow-firefly --- .../workflow-firefly/action-binder.js | 114 +++++++----------- 1 file changed, 41 insertions(+), 73 deletions(-) diff --git a/unitylibs/core/workflow/workflow-firefly/action-binder.js b/unitylibs/core/workflow/workflow-firefly/action-binder.js index 364590fd9..e1ace56d9 100644 --- a/unitylibs/core/workflow/workflow-firefly/action-binder.js +++ b/unitylibs/core/workflow/workflow-firefly/action-binder.js @@ -14,61 +14,7 @@ import { getLocale, sendAnalyticsEvent, } from '../../../scripts/utils.js'; - -class ServiceHandler { - constructor(renderWidget = false, canvasArea = null, unityEl = null) { - this.renderWidget = renderWidget; - this.canvasArea = canvasArea; - this.unityEl = unityEl; - } - - async fetchFromService(url, options) { - try { - const response = await fetch(url, options); - const error = new Error(); - if (response.status !== 200) { - error.status = response.status; - throw error; - } - return response.json(); - } catch (error) { - if (error.name === 'TimeoutError' || error.name === 'AbortError') { - error.status = 504; - } - throw error; - } - } - - async postCallToService(api, options, unityProduct, unityAction) { - const postOpts = { - method: 'POST', - headers: await getHeaders(unityConfig.apiKey, { - 'x-unity-product': unityProduct, - 'x-unity-action': unityAction, - }), - ...options, - }; - return this.fetchFromService(api, postOpts); - } - - showErrorToast(errorCallbackOptions, error, lanaOptions, errorType = 'server') { - sendAnalyticsEvent(new CustomEvent(`FF Generate prompt ${errorType} error|UnityWidget`)); - if (!errorCallbackOptions?.errorToastEl) return; - const lang = document.querySelector('html').getAttribute('lang'); - const msg = lang !== 'ja-JP' ? this.unityEl.querySelector(errorCallbackOptions.errorType)?.nextSibling.textContent : this.unityEl.querySelector(errorCallbackOptions.errorType)?.parentElement.textContent; - const promptBarEl = this.canvasArea.querySelector('.copy .ex-unity-wrap'); - if (promptBarEl) promptBarEl.style.pointerEvents = 'none'; - const errorToast = promptBarEl.querySelector('.alert-holder'); - if (!errorToast) return; - const closeBtn = errorToast.querySelector('.alert-close'); - if (closeBtn) closeBtn.style.pointerEvents = 'auto'; - const alertText = errorToast.querySelector('.alert-text p'); - if (!alertText) return; - alertText.innerText = msg; - errorToast.classList.add('show'); - window.lana?.log(`Message: ${msg}, Error: ${error || ''}`, lanaOptions); - } -} +import NetworkUtils from '../../../utils/NetworkUtils.js'; export default class ActionBinder { static VALID_KEYS = ['Tab', 'ArrowDown', 'ArrowUp', 'Enter', 'Escape', ' ']; @@ -84,7 +30,6 @@ export default class ActionBinder { this.canvasArea = canvasArea; this.actions = actionMap; this.query = ''; - this.serviceHandler = null; this.activeIndex = -1; this.id = ''; this.apiConfig = { ...unityConfig }; @@ -98,8 +43,7 @@ export default class ActionBinder { const run = async () => { try { if (!this.errorToastEl) this.errorToastEl = await this.createErrorToast(); - if (!this.serviceHandler) await this.loadServiceHandler(); - this.serviceHandler?.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-audio-fail' }, ev?.detail?.error, this.lanaOptions, 'client'); + this.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-audio-fail' }, ev?.detail?.error, this.lanaOptions, 'client'); } catch (e) { /* noop */ } }; run(); @@ -109,10 +53,29 @@ export default class ActionBinder { this.errorToastEl = null; this.lanaOptions = { sampleRate: 1, tags: 'Unity-FF' }; this.sendAnalyticsToSplunk = null; + this.networkUtils = new NetworkUtils(); this.addAccessibility(); this.initAction(); } + showErrorToast(errorCallbackOptions, error, lanaOptions, errorType = 'server') { + sendAnalyticsEvent(new CustomEvent(`FF Generate prompt ${errorType} error|UnityWidget`)); + if (!errorCallbackOptions?.errorToastEl) return; + const lang = document.querySelector('html').getAttribute('lang'); + const msg = lang !== 'ja-JP' ? this.unityEl.querySelector(errorCallbackOptions.errorType)?.nextSibling.textContent : this.unityEl.querySelector(errorCallbackOptions.errorType)?.parentElement.textContent; + const promptBarEl = this.canvasArea.querySelector('.copy .ex-unity-wrap'); + if (promptBarEl) promptBarEl.style.pointerEvents = 'none'; + const errorToast = promptBarEl.querySelector('.alert-holder'); + if (!errorToast) return; + const closeBtn = errorToast.querySelector('.alert-close'); + if (closeBtn) closeBtn.style.pointerEvents = 'auto'; + const alertText = errorToast.querySelector('.alert-text p'); + if (!alertText) return; + alertText.innerText = msg; + errorToast.classList.add('show'); + window.lana?.log(`Message: ${msg}, Error: ${error || ''}`, lanaOptions); + } + async initAction() { if (!this.errorToastEl) this.errorToastEl = await this.createErrorToast(); const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent) @@ -186,14 +149,6 @@ export default class ActionBinder { }); } - async loadServiceHandler() { - this.serviceHandler = new ServiceHandler( - this.workflowCfg.targetCfg.renderWidget, - this.canvasArea, - this.unityEl, - ); - } - addEventListeners(el, actionsList) { const handleClick = async (event) => { event.preventDefault(); @@ -256,7 +211,7 @@ export default class ActionBinder { validateInput(query) { if (query.length > 750) { - this.serviceHandler.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-max-length' }, 'Max prompt characters exceeded'); + this.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-max-length' }, 'Max prompt characters exceeded'); return { isValid: false, errorCode: 'max-prompt-characters-exceeded' }; } return { isValid: true }; @@ -279,7 +234,6 @@ export default class ActionBinder { async generateContent() { await this.initAnalytics(); - if (!this.serviceHandler) await this.loadServiceHandler(); const cgen = this.unityEl.querySelector('.icon-cgen')?.nextSibling?.textContent?.trim(); const queryParams = {}; if (cgen) { @@ -321,11 +275,25 @@ export default class ActionBinder { }, ...(this.id ? { assetId: this.id } : { query: this.query }), }; - const { url } = await this.serviceHandler.postCallToService( + const postOpts = { + method: 'POST', + headers: await getHeaders(unityConfig.apiKey, { + 'x-unity-product': this.workflowCfg.productName, + 'x-unity-action': `${action}-${this.getSelectedVerbType()}Generation`, + }), + body: JSON.stringify(payload), + }; + const { url } = await this.networkUtils.fetchFromService( this.apiConfig.connectorApiEndPoint, - { body: JSON.stringify(payload) }, - this.workflowCfg.productName, - `${action}-${this.getSelectedVerbType()}Generation`, + postOpts, + async (response) => { + if (response.status !== 200) { + const error = new Error(); + error.status = response.status; + throw error; + } + return response.json(); + }, ); this.logAnalytics('generate', eventData, { workflowStep: 'complete', statusCode: 0 }); this.query = ''; @@ -334,7 +302,7 @@ export default class ActionBinder { if (url) window.location.href = url; } catch (err) { this.query = ''; - this.serviceHandler.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-request' }, err); + this.showErrorToast({ errorToastEl: this.errorToastEl, errorType: '.icon-error-request' }, err); this.logAnalytics('generate', { ...eventData, errorData: { code: 'request-failed', subCode: err.status, desc: err.message }, From 14c4006b70a21edac48fe2ab0a0840e676b98d59 Mon Sep 17 00:00:00 2001 From: vipulg Date: Mon, 5 Jan 2026 14:17:24 +0530 Subject: [PATCH 2/4] updating UTs --- test/core/workflow/workflow.firefly.test.js | 42 +++++++++------------ 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/test/core/workflow/workflow.firefly.test.js b/test/core/workflow/workflow.firefly.test.js index 622068fca..f0207ec15 100644 --- a/test/core/workflow/workflow.firefly.test.js +++ b/test/core/workflow/workflow.firefly.test.js @@ -236,19 +236,18 @@ describe('Firefly Workflow Tests', () => { it('should handle generateContent with network errors', async () => { actionBinder.inputField.value = 'valid query'; - const mockServiceHandler = { - postCallToService: sinon.stub().rejects(new Error('Network error')), - showErrorToast: sinon.stub(), - }; - actionBinder.serviceHandler = mockServiceHandler; + const fetchStub = sinon.stub(actionBinder.networkUtils, 'fetchFromService').rejects(new Error('Network error')); + const showErrorToastStub = sinon.stub(actionBinder, 'showErrorToast'); const logAnalyticsStub = sinon.stub(actionBinder, 'logAnalytics'); await actionBinder.generateContent(); - expect(mockServiceHandler.showErrorToast.calledOnce).to.be.true; + expect(showErrorToastStub.calledOnce).to.be.true; expect(logAnalyticsStub.calledTwice).to.be.true; expect(logAnalyticsStub.secondCall.args[2].statusCode).to.equal(-1); + fetchStub.restore(); + showErrorToastStub.restore(); logAnalyticsStub.restore(); }); @@ -2229,38 +2228,41 @@ describe('Firefly Workflow Tests', () => { // Mock dependencies sinon.stub(testActionBinder, 'initAnalytics').resolves(); - sinon.stub(testActionBinder, 'loadServiceHandler').resolves(); sinon.stub(testActionBinder, 'validateInput').returns({ isValid: true }); sinon.stub(testActionBinder, 'logAnalytics'); - sinon.stub(testActionBinder, 'resetDropdown'); + const resetDropdownStub = sinon.stub(testActionBinder, 'resetDropdown'); - // Mock serviceHandler - testActionBinder.serviceHandler = { postCallToService: sinon.stub().resolves({ success: true }) }; + // Mock network call + const fetchStub = sinon.stub(testActionBinder.networkUtils, 'fetchFromService').resolves({}); const input = mockBlock.querySelector('.inp-field'); input.value = 'test query'; await testActionBinder.generateContent(); - expect(testActionBinder.serviceHandler.postCallToService.calledOnce).to.be.true; + expect(testActionBinder.query).to.equal(''); + expect(testActionBinder.id).to.equal('test-asset-id'); + fetchStub.restore(); + resetDropdownStub.restore(); }); it('should handle generateContent error', async () => { // Mock dependencies sinon.stub(testActionBinder, 'initAnalytics').resolves(); - sinon.stub(testActionBinder, 'loadServiceHandler').resolves(); sinon.stub(testActionBinder, 'validateInput').returns({ isValid: true }); sinon.stub(testActionBinder, 'logAnalytics'); - // Mock serviceHandler to throw error - testActionBinder.serviceHandler = { postCallToService: sinon.stub().rejects(new Error('Service error')), showErrorToast: sinon.stub() }; + // Mock network to throw error + sinon.stub(testActionBinder.networkUtils, 'fetchFromService').rejects(new Error('Service error')); + const showErrorToastStub = sinon.stub(testActionBinder, 'showErrorToast'); const input = mockBlock.querySelector('.inp-field'); input.value = 'test query'; await testActionBinder.generateContent(); - expect(testActionBinder.serviceHandler.showErrorToast.calledOnce).to.be.true; + expect(showErrorToastStub.calledOnce).to.be.true; + showErrorToastStub.restore(); }); it('should handle initializeApiConfig', () => { @@ -2268,16 +2270,6 @@ describe('Firefly Workflow Tests', () => { expect(result).to.be.an('object'); }); - it('should handle loadServiceHandler', async () => { - // Ensure targetCfg exists - if (!testActionBinder.workflowCfg.targetCfg) { - testActionBinder.workflowCfg.targetCfg = {}; - } - testActionBinder.workflowCfg.targetCfg.renderWidget = true; - await testActionBinder.loadServiceHandler(); - expect(testActionBinder.serviceHandler).to.be.an('object'); - }); - it('should handle addEventListeners for A element', () => { const link = document.createElement('a'); link.href = '#'; From 54895d497bae7337f6a592c009569cc4475aaa8b Mon Sep 17 00:00:00 2001 From: vipulg Date: Wed, 7 Jan 2026 17:11:11 +0530 Subject: [PATCH 3/4] minor update --- .../workflow/workflow-firefly/action-binder.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/unitylibs/core/workflow/workflow-firefly/action-binder.js b/unitylibs/core/workflow/workflow-firefly/action-binder.js index e1ace56d9..efa42e6dc 100644 --- a/unitylibs/core/workflow/workflow-firefly/action-binder.js +++ b/unitylibs/core/workflow/workflow-firefly/action-binder.js @@ -10,7 +10,7 @@ import { createTag, defineDeviceByScreenSize, getLibs, - getHeaders, + getApiCallOptions, getLocale, sendAnalyticsEvent, } from '../../../scripts/utils.js'; @@ -275,14 +275,15 @@ export default class ActionBinder { }, ...(this.id ? { assetId: this.id } : { query: this.query }), }; - const postOpts = { - method: 'POST', - headers: await getHeaders(unityConfig.apiKey, { + const postOpts = await getApiCallOptions( + 'POST', + unityConfig.apiKey, + { 'x-unity-product': this.workflowCfg.productName, 'x-unity-action': `${action}-${this.getSelectedVerbType()}Generation`, - }), - body: JSON.stringify(payload), - }; + }, + { body: JSON.stringify(payload) }, + ); const { url } = await this.networkUtils.fetchFromService( this.apiConfig.connectorApiEndPoint, postOpts, From e7e15f68ddd8689f08f81d335be3daf53eecb0a9 Mon Sep 17 00:00:00 2001 From: vipulg Date: Tue, 13 Jan 2026 13:30:47 +0530 Subject: [PATCH 4/4] review comment --- test/core/workflow/workflow.firefly.test.js | 14 +++++++++----- .../workflow/workflow-firefly/action-binder.js | 11 ++++++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/test/core/workflow/workflow.firefly.test.js b/test/core/workflow/workflow.firefly.test.js index f0207ec15..53f743bc6 100644 --- a/test/core/workflow/workflow.firefly.test.js +++ b/test/core/workflow/workflow.firefly.test.js @@ -236,7 +236,8 @@ describe('Firefly Workflow Tests', () => { it('should handle generateContent with network errors', async () => { actionBinder.inputField.value = 'valid query'; - const fetchStub = sinon.stub(actionBinder.networkUtils, 'fetchFromService').rejects(new Error('Network error')); + const fetchStub = sinon.stub().rejects(new Error('Network error')); + const getNetStub = sinon.stub(actionBinder, 'getNetworkUtils').resolves({ fetchFromService: fetchStub }); const showErrorToastStub = sinon.stub(actionBinder, 'showErrorToast'); const logAnalyticsStub = sinon.stub(actionBinder, 'logAnalytics'); @@ -246,7 +247,7 @@ describe('Firefly Workflow Tests', () => { expect(logAnalyticsStub.calledTwice).to.be.true; expect(logAnalyticsStub.secondCall.args[2].statusCode).to.equal(-1); - fetchStub.restore(); + getNetStub.restore(); showErrorToastStub.restore(); logAnalyticsStub.restore(); }); @@ -2233,7 +2234,8 @@ describe('Firefly Workflow Tests', () => { const resetDropdownStub = sinon.stub(testActionBinder, 'resetDropdown'); // Mock network call - const fetchStub = sinon.stub(testActionBinder.networkUtils, 'fetchFromService').resolves({}); + const fetchStub = sinon.stub().resolves({}); + const getNetStub = sinon.stub(testActionBinder, 'getNetworkUtils').resolves({ fetchFromService: fetchStub }); const input = mockBlock.querySelector('.inp-field'); input.value = 'test query'; @@ -2242,7 +2244,7 @@ describe('Firefly Workflow Tests', () => { expect(testActionBinder.query).to.equal(''); expect(testActionBinder.id).to.equal('test-asset-id'); - fetchStub.restore(); + getNetStub.restore(); resetDropdownStub.restore(); }); @@ -2253,7 +2255,8 @@ describe('Firefly Workflow Tests', () => { sinon.stub(testActionBinder, 'logAnalytics'); // Mock network to throw error - sinon.stub(testActionBinder.networkUtils, 'fetchFromService').rejects(new Error('Service error')); + const fetchStub = sinon.stub().rejects(new Error('Service error')); + const getNetStub = sinon.stub(testActionBinder, 'getNetworkUtils').resolves({ fetchFromService: fetchStub }); const showErrorToastStub = sinon.stub(testActionBinder, 'showErrorToast'); const input = mockBlock.querySelector('.inp-field'); @@ -2263,6 +2266,7 @@ describe('Firefly Workflow Tests', () => { expect(showErrorToastStub.calledOnce).to.be.true; showErrorToastStub.restore(); + getNetStub.restore(); }); it('should handle initializeApiConfig', () => { diff --git a/unitylibs/core/workflow/workflow-firefly/action-binder.js b/unitylibs/core/workflow/workflow-firefly/action-binder.js index efa42e6dc..f63639af1 100644 --- a/unitylibs/core/workflow/workflow-firefly/action-binder.js +++ b/unitylibs/core/workflow/workflow-firefly/action-binder.js @@ -14,7 +14,6 @@ import { getLocale, sendAnalyticsEvent, } from '../../../scripts/utils.js'; -import NetworkUtils from '../../../utils/NetworkUtils.js'; export default class ActionBinder { static VALID_KEYS = ['Tab', 'ArrowDown', 'ArrowUp', 'Enter', 'Escape', ' ']; @@ -53,11 +52,16 @@ export default class ActionBinder { this.errorToastEl = null; this.lanaOptions = { sampleRate: 1, tags: 'Unity-FF' }; this.sendAnalyticsToSplunk = null; - this.networkUtils = new NetworkUtils(); this.addAccessibility(); this.initAction(); } + getNetworkUtils = async () => { + if (this.networkUtils) return this.networkUtils; + const { default: NetworkUtils } = await import(`${getUnityLibs()}/utils/NetworkUtils.js`); + return (this.networkUtils = new NetworkUtils()); + }; + showErrorToast(errorCallbackOptions, error, lanaOptions, errorType = 'server') { sendAnalyticsEvent(new CustomEvent(`FF Generate prompt ${errorType} error|UnityWidget`)); if (!errorCallbackOptions?.errorToastEl) return; @@ -284,7 +288,8 @@ export default class ActionBinder { }, { body: JSON.stringify(payload) }, ); - const { url } = await this.networkUtils.fetchFromService( + const networkUtils = await this.getNetworkUtils(); + const { url } = await networkUtils.fetchFromService( this.apiConfig.connectorApiEndPoint, postOpts, async (response) => {