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
136 changes: 130 additions & 6 deletions test/core/workflow/workflow-acrobat/action-binder.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from '@esm-bundle/chai';
import sinon from 'sinon';
import ActionBinder from '../../../../unitylibs/core/workflow/workflow-acrobat/action-binder.js';
import { getUnityLibs, setUnityLibs } from '../../../../unitylibs/scripts/utils.js';

const mockUpdateProgressBar = null;

Expand Down Expand Up @@ -1557,11 +1558,7 @@ describe('ActionBinder', () => {
});

describe('continueInApp', () => {
let locationSpy;

beforeEach(() => {
locationSpy = sinon.spy();

actionBinder.redirectUrl = 'https://test.com?param=value';
actionBinder.operations = ['test-operation'];
actionBinder.redirectWithoutUpload = false;
Expand All @@ -1570,6 +1567,7 @@ describe('ActionBinder', () => {
actionBinder.multiFileFailure = null;
actionBinder.showTransitionScreen = sinon.stub().resolves();
actionBinder.transitionScreen = {
clearProgressBarHandler: sinon.stub(),
updateProgressBar: sinon.stub(),
showSplashScreen: sinon.stub().resolves(),
};
Expand All @@ -1581,14 +1579,16 @@ describe('ActionBinder', () => {
actionBinder.redirectUrl = '';
await actionBinder.continueInApp();
expect(actionBinder.showTransitionScreen.called).to.be.false;
expect(locationSpy.called).to.be.false;
expect(actionBinder.transitionScreen.updateProgressBar.called).to.be.false;
expect(actionBinder.delay.called).to.be.false;
});

it('should not proceed if no operations and not redirectWithoutUpload', async () => {
actionBinder.operations = [];
await actionBinder.continueInApp();
expect(actionBinder.showTransitionScreen.called).to.be.false;
expect(locationSpy.called).to.be.false;
expect(actionBinder.transitionScreen.updateProgressBar.called).to.be.false;
expect(actionBinder.delay.called).to.be.false;
});

it('should log to splunk and continue when direct upload verb progress bar update throws', async () => {
Expand Down Expand Up @@ -1635,13 +1635,113 @@ describe('ActionBinder', () => {
showSplashScreen: sinon.stub().resolves(),
};
actionBinder.transitionScreen = existingTransitionScreen;
// Stop after the progress update; this test does not exercise navigation.
actionBinder.delay.callsFake(async () => { actionBinder.redirectUrl = ''; });
await actionBinder.continueInApp();
expect(actionBinder.transitionScreen).to.equal(existingTransitionScreen);
expect(actionBinder.LOADER_LIMIT).to.equal(100);
expect(existingTransitionScreen.LOADER_LIMIT).to.equal(100);
expect(existingTransitionScreen.clearProgressBarHandler.calledOnce).to.be.true;
expect(existingTransitionScreen.updateProgressBar.calledOnceWith(splashLayer, 100)).to.be.true;
});

it('should restore the splash and report an error when the final redirect delay fails', async () => {
const error = new Error('redirect delay failed');
actionBinder.delay.rejects(error);
await actionBinder.continueInApp();
expect(actionBinder.transitionScreen.showSplashScreen.calledOnce).to.be.true;
expect(actionBinder.dispatchErrorToast.calledOnceWith(
'error_generic',
500,
'Exception thrown when redirecting to product; redirect delay failed',
false,
undefined,
{ code: 'upload_error_redirect_to_app', subCode: undefined, desc: error.message },
)).to.be.true;
});

it('should not navigate when cancel clears the redirect URL during the final progress delay', async () => {
actionBinder.transitionScreen = {
splashScreenEl: document.createElement('div'),
clearProgressBarHandler: sinon.stub(),
updateProgressBar: sinon.stub(),
showSplashScreen: sinon.stub().resolves(),
};
actionBinder.delay = sinon.stub().callsFake(async () => { actionBinder.redirectUrl = ''; });
// multiFileFailure is only read when building the navigation URL. Throwing here makes a
// regression fail via the catch/toast path instead of actually navigating the test page.
Object.defineProperty(actionBinder, 'multiFileFailure', {
get() { throw new Error('navigation attempted after cancel'); },
configurable: true,
});
try {
await actionBinder.continueInApp();
expect(actionBinder.dispatchErrorToast.called).to.be.false;
expect(actionBinder.transitionScreen.showSplashScreen.called).to.be.false;
} finally {
delete actionBinder.multiFileFailure;
}
});
});

describe('showTransitionScreen', () => {
it('should clear the previous transition screen progress bar timer before loading a new one', async () => {
setUnityLibs('/unitylibs');
const splashLayer = document.createElement('div');
const existingTransitionScreen = {
splashScreenEl: splashLayer,
clearProgressBarHandler: sinon.stub(),
};
actionBinder.transitionScreen = existingTransitionScreen;
const pending = actionBinder.showTransitionScreen();
expect(existingTransitionScreen.clearProgressBarHandler.calledOnce).to.be.true;
await pending;
expect(actionBinder.transitionScreen).to.not.equal(existingTransitionScreen);
expect(actionBinder.transitionScreen.splashScreenEl).to.equal(splashLayer);
expect(actionBinder.transitionScreen.LOADER_LIMIT).to.equal(actionBinder.LOADER_LIMIT);
expect(actionBinder.transitionScreen.showSplashScreen).to.be.a('function');
});
});

describe('loadTransitionScreen', () => {
beforeEach(() => {
setUnityLibs('/unitylibs');
actionBinder.loadTransitionScreen.restore();
});

it('should load the transition screen only once and schedule its splash loader', async () => {
const { default: TransitionScreen } = await import(`${getUnityLibs()}/scripts/transition-screen.js`);
const loader = sinon.stub(TransitionScreen.prototype, 'delayedSplashLoader').resolves();
await actionBinder.loadTransitionScreen();
const { transitionScreen } = actionBinder;
await actionBinder.loadTransitionScreen();
expect(transitionScreen).to.be.instanceOf(TransitionScreen);
expect(transitionScreen.workflowCfg).to.equal(mockWorkflowCfg);
expect(actionBinder.transitionScreen).to.equal(transitionScreen);
expect(loader.calledOnce).to.be.true;
});

it('should report and propagate a splash loader failure', async () => {
const { default: TransitionScreen } = await import(`${getUnityLibs()}/scripts/transition-screen.js`);
const error = new Error('splash loader failed');
sinon.stub(TransitionScreen.prototype, 'delayedSplashLoader').rejects(error);
const toast = sinon.stub(actionBinder, 'dispatchErrorToast').resolves();
let failure;
try {
await actionBinder.loadTransitionScreen();
} catch (caught) {
failure = caught;
}
expect(failure).to.equal(error);
expect(toast.calledOnceWith(
'pre_upload_error_transition_screen',
null,
`Error loading transition screen, Error: ${error}`,
false,
true,
{ code: 'pre_upload_error_transition_screen' },
)).to.be.true;
});
});

describe('cancelAcrobatOperation', () => {
Expand Down Expand Up @@ -1703,6 +1803,12 @@ describe('ActionBinder', () => {
await actionBinder.cancelAcrobatOperation();
expect(actionBinder.filesData.workflowStep).to.equal('preuploading');
});

it('should clear recorded operations so the next upload cannot redirect with stale state', async () => {
actionBinder.operations = ['asset-cancelled'];
await actionBinder.cancelAcrobatOperation();
expect(actionBinder.operations).to.deep.equal([]);
});
});

describe('initActionListeners', () => {
Expand Down Expand Up @@ -2038,6 +2144,24 @@ describe('ActionBinder', () => {
spy.restore();
});

it('should register the RedirectReady listener only once across repeated actions', async () => {
actionBinder.transitionScreen = { test: 'existing' };
actionBinder.handlePreloads = sinon.stub().resolves();
const cancelStub = sinon.stub(actionBinder, 'cancelAcrobatOperation').resolves();
const addSpy = sinon.spy(window, 'addEventListener');
delete actionBinder.redirectReadyBound;
try {
await actionBinder.acrobatActionMaps('interrupt');
await actionBinder.acrobatActionMaps('interrupt');
await actionBinder.acrobatActionMaps('interrupt');
const registrations = addSpy.getCalls().filter((c) => c.args[0] === 'DCUnity:RedirectReady');
expect(registrations).to.have.lengthOf(1);
} finally {
addSpy.restore();
cancelStub.restore();
}
});

describe('enabledFeatures validation', () => {
it('should dispatch error when enabledFeatures is null', async () => {
// Mock transition screen to avoid early return
Expand Down
89 changes: 89 additions & 0 deletions test/core/workflow/workflow-acrobat/upload-handler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,95 @@ describe('UploadHandler', () => {
});
});

describe('directUploadSingleFile', () => {
let file;
let fileData;

beforeEach(() => {
file = new File(['test content'], 'test.pdf', { type: 'application/pdf' });
fileData = { type: 'application/pdf', size: 1000, count: 1, uploadType: 'sfu' };
});

it('should show the error splash screen and return false for a genuine upload failure', async () => {
uploadHandler.directUploadAsset = sinon.stub().rejects(new Error('network fail'));

const result = await uploadHandler.directUploadSingleFile(file, fileData);

expect(result).to.be.false;
expect(uploadHandler.initSplashScreen.called).to.be.true;
expect(mockTransitionScreen.showSplashScreen.called).to.be.true;
expect(mockActionBinder.dispatchErrorToast.called).to.be.true;
});

it('should redirect and dispatch the uploaded event when the upload completes normally', async () => {
mockActionBinder.isUploading = true;
uploadHandler.directUploadAsset = sinon.stub().resolves({ id: 'asset-999' });

const result = await uploadHandler.directUploadSingleFile(file, fileData);

expect(result).to.be.true;
expect(mockActionBinder.handleRedirect.calledOnce).to.be.true;
expect(mockActionBinder.operations).to.include('asset-999');
expect(mockActionBinder.dispatchAnalyticsEvent.calledWith('uploaded')).to.be.true;
});

it('should skip the redirect when cancel wins the race against a completing upload', async () => {
// Upload can still resolve after Cancel already ran.
mockActionBinder.isUploading = false;
uploadHandler.directUploadAsset = sinon.stub().resolves({ id: 'asset-999' });

const result = await uploadHandler.directUploadSingleFile(file, fileData);

expect(result).to.be.true;
expect(mockActionBinder.handleRedirect.called).to.be.false;
expect(mockActionBinder.operations).to.not.include('asset-999');
expect(mockActionBinder.dispatchAnalyticsEvent.calledWith('uploaded')).to.be.false;
});

it('should stay silent and skip the chunked fallback when cancel aborts the upload', async () => {
const abortError = new Error('Request aborted by user.');
abortError.name = 'AbortError';
uploadHandler.directUploadAsset = sinon.stub().rejects(abortError);

const result = await uploadHandler.directUploadSingleFile(file, fileData);

expect(result).to.be.true;
expect(uploadHandler.initSplashScreen.called).to.be.false;
expect(mockActionBinder.dispatchErrorToast.called).to.be.false;
});

it('should still show the error splash screen for a request timeout (not a user cancel)', async () => {
const timeoutError = new Error('Request timed out after 60000ms');
timeoutError.name = 'TimeoutError';
uploadHandler.directUploadAsset = sinon.stub().rejects(timeoutError);

const result = await uploadHandler.directUploadSingleFile(file, fileData);

expect(result).to.be.false;
expect(uploadHandler.initSplashScreen.called).to.be.true;
expect(mockActionBinder.dispatchErrorToast.called).to.be.true;
});

it('should not record a cancelled upload whose redirect URL returns after a new upload started', async () => {
const signal = { aborted: false };
mockActionBinder.getAbortSignal = sinon.stub().returns(signal);
mockActionBinder.isUploading = true;
uploadHandler.directUploadAsset = sinon.stub().resolves({ id: 'asset-cancelled' });
mockActionBinder.handleRedirect = sinon.stub().callsFake(async () => {
// Cancel aborts this upload, then the next upload sets isUploading back to true.
signal.aborted = true;
mockActionBinder.isUploading = true;
return true;
});

const result = await uploadHandler.directUploadSingleFile(file, fileData);

expect(result).to.be.true;
expect(mockActionBinder.operations).to.not.include('asset-cancelled');
expect(mockActionBinder.dispatchAnalyticsEvent.calledWith('uploaded')).to.be.false;
});
});

describe('uploadMultiFile', () => {
let files;
let filesData;
Expand Down
Loading
Loading