diff --git a/test/core/workflow/workflow.firefly.test.js b/test/core/workflow/workflow.firefly.test.js index 622068fca..53f743bc6 100644 --- a/test/core/workflow/workflow.firefly.test.js +++ b/test/core/workflow/workflow.firefly.test.js @@ -236,19 +236,19 @@ 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().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'); 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); + getNetStub.restore(); + showErrorToastStub.restore(); logAnalyticsStub.restore(); }); @@ -2229,38 +2229,44 @@ 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().resolves({}); + const getNetStub = sinon.stub(testActionBinder, 'getNetworkUtils').resolves({ fetchFromService: fetchStub }); 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'); + getNetStub.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 + 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'); input.value = 'test query'; await testActionBinder.generateContent(); - expect(testActionBinder.serviceHandler.showErrorToast.calledOnce).to.be.true; + expect(showErrorToastStub.calledOnce).to.be.true; + showErrorToastStub.restore(); + getNetStub.restore(); }); it('should handle initializeApiConfig', () => { @@ -2268,16 +2274,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 = '#'; diff --git a/unitylibs/core/workflow/workflow-firefly/action-binder.js b/unitylibs/core/workflow/workflow-firefly/action-binder.js index 364590fd9..f63639af1 100644 --- a/unitylibs/core/workflow/workflow-firefly/action-binder.js +++ b/unitylibs/core/workflow/workflow-firefly/action-binder.js @@ -10,66 +10,11 @@ import { createTag, defineDeviceByScreenSize, getLibs, - getHeaders, + getApiCallOptions, 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); - } -} - export default class ActionBinder { static VALID_KEYS = ['Tab', 'ArrowDown', 'ArrowUp', 'Enter', 'Escape', ' ']; @@ -84,7 +29,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 +42,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(); @@ -113,6 +56,30 @@ export default class ActionBinder { 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; + 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 +153,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 +215,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 +238,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 +279,27 @@ export default class ActionBinder { }, ...(this.id ? { assetId: this.id } : { query: this.query }), }; - const { url } = await this.serviceHandler.postCallToService( - this.apiConfig.connectorApiEndPoint, + const postOpts = await getApiCallOptions( + 'POST', + unityConfig.apiKey, + { + 'x-unity-product': this.workflowCfg.productName, + 'x-unity-action': `${action}-${this.getSelectedVerbType()}Generation`, + }, { body: JSON.stringify(payload) }, - this.workflowCfg.productName, - `${action}-${this.getSelectedVerbType()}Generation`, + ); + const networkUtils = await this.getNetworkUtils(); + const { url } = await networkUtils.fetchFromService( + this.apiConfig.connectorApiEndPoint, + 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 +308,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 },