Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 21 additions & 25 deletions test/core/workflow/workflow.firefly.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down Expand Up @@ -2229,55 +2229,51 @@ 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', () => {
const result = testActionBinder.initializeApiConfig();
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 = '#';
Expand Down
122 changes: 48 additions & 74 deletions unitylibs/core/workflow/workflow-firefly/action-binder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', ' '];

Expand All @@ -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 };
Expand All @@ -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();
Expand All @@ -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)
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 };
Expand All @@ -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) {
Expand Down Expand Up @@ -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 = '';
Expand All @@ -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 },
Expand Down
Loading