Skip to content
Open
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openstack-uicore-foundation",
"version": "5.0.56",
"version": "5.0.58-beta.0",
"description": "ui reactjs components for openstack marketing site",
"main": "lib/openstack-uicore-foundation.js",
"scripts": {
Expand Down
261 changes: 258 additions & 3 deletions src/components/inputs/dropzone/__tests__/dropzone.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,22 @@ let mockCapturedOptions = {};
jest.mock('dropzone', () => {
return jest.fn().mockImplementation((element, options) => {
mockCapturedOptions = options;
return {
const dz = {
options,
on: jest.fn(),
off: jest.fn(),
destroy: jest.fn(() => null),
getActiveFiles: jest.fn(() => [])
};
// Mimics Dropzone's real Emitter: replays every handler registered via `on`
// for that event, in registration order - the same "multiple listeners on
// one event" behavior the ontimeout/pollUploadStatus fixes rely on.
dz.emit = jest.fn((event, ...args) => {
dz.on.mock.calls
.filter(([evt]) => evt === event)
.forEach(([, handler]) => handler(...args));
});
return dz;
});
});

Expand Down Expand Up @@ -183,6 +192,7 @@ describe('DropzoneJS - HTTP 202 Polling UX', () => {
// Mock fetch to return complete status
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({
status: 'complete',
name: 'test.pdf',
Expand Down Expand Up @@ -244,6 +254,7 @@ describe('DropzoneJS - HTTP 202 Polling UX', () => {
test('test_dropzone_poll_upload_status_encodes_file_id_in_url', (done) => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({
status: 'complete',
name: 'HPE_OCPSanJose_Backdrop_20x10ft50_Ver1.4_PRINT.pdf',
Expand Down Expand Up @@ -308,7 +319,7 @@ describe('DropzoneJS - HTTP 202 Polling UX', () => {
test('test_dropzone_cancel_stops_polling_for_that_file_only', (done) => {
// Server is still processing: every poll answers 'uploading', so polling keeps going.
global.fetch = jest.fn(() =>
Promise.resolve({ json: () => Promise.resolve({ status: 'uploading' }) })
Promise.resolve({ ok: true, json: () => Promise.resolve({ status: 'uploading' }) })
);

const ref = React.createRef();
Expand Down Expand Up @@ -369,6 +380,7 @@ describe('DropzoneJS - HTTP 202 Polling UX', () => {
new Promise((resolve) => {
respondComplete = () =>
resolve({
ok: true,
json: () =>
Promise.resolve({ status: 'complete', name: 'test.pdf', size: 1024000 })
});
Expand Down Expand Up @@ -424,7 +436,7 @@ describe('DropzoneJS - HTTP 202 Polling UX', () => {
*/
test('test_dropzone_unmount_stops_polling_for_every_file', (done) => {
global.fetch = jest.fn(() =>
Promise.resolve({ json: () => Promise.resolve({ status: 'uploading' }) })
Promise.resolve({ ok: true, json: () => Promise.resolve({ status: 'uploading' }) })
);

const ref = React.createRef();
Expand Down Expand Up @@ -515,6 +527,249 @@ describe('DropzoneJS - HTTP 202 Polling UX', () => {
done();
}, 10);
});

/**
* Test Case 10: a chunk that times out releases its concurrency slot
*
* Dropzone's own default xhr.ontimeout (set before the 'sending' handler runs)
* still fires 'error', but without wrapping it here onChunkComplete() never
* runs, so chunksInFlight never decrements and later chunks stay queued forever.
*/
test('test_dropzone_ontimeout_releases_chunk_slot', () => {
const ref = React.createRef();

render(
<DropzoneJS
{...defaultProps}
ref={ref}
onUploadComplete={onUploadCompleteMock}
onError={onErrorMock}
/>
);

const instance = ref.current;
// Only requests that went through the chunk-throttle queue occupy a concurrency slot.
const mockFile = { name: 'test.pdf', size: 1024000, _isThrottledChunk: true };
const dropzoneOnTimeout = jest.fn();
const mockXhr = {
readyState: XMLHttpRequest.DONE,
setRequestHeader: jest.fn(),
onload: jest.fn(),
onerror: jest.fn(),
ontimeout: dropzoneOnTimeout,
abort: jest.fn()
};

instance.chunksInFlight = 1;
getEventHandler(instance, 'sending')(mockFile, mockXhr, { append: jest.fn() });

mockXhr.ontimeout({});

expect(instance.chunksInFlight).toBe(0);
// Dropzone's own timeout handling (which still reports the error) must still run.
expect(dropzoneOnTimeout).toHaveBeenCalledTimes(1);
});

/**
* Test Case 10b: a non-chunked upload's completion must not free a concurrency slot
* it never occupied.
*
* chunksInFlight is only ever incremented for requests routed through the chunk-throttle
* queue (setupChunkThrottle). A file that bypasses the queue (no _isThrottledChunk tag)
* finishing its single request used to still call onChunkComplete() unconditionally,
* decrementing the counter for chunked uploads that were still genuinely in flight and
* letting more than maxConcurrentChunks run at once.
*/
test('test_dropzone_non_chunked_upload_does_not_release_a_chunk_slot', () => {
const ref = React.createRef();

render(
<DropzoneJS
{...defaultProps}
ref={ref}
onUploadComplete={onUploadCompleteMock}
onError={onErrorMock}
/>
);

const instance = ref.current;
// No _isThrottledChunk tag - this file bypassed the throttle queue.
const mockFile = { name: 'small.pdf', size: 1024 };
const mockXhr = {
readyState: XMLHttpRequest.DONE,
status: 200,
responseText: JSON.stringify({ name: 'small.pdf', path: 'uploads/', size: 1024 }),
setRequestHeader: jest.fn(),
onload: jest.fn(),
onerror: jest.fn(),
abort: jest.fn()
};

// Two real chunked uploads for a different file are genuinely in flight.
instance.chunksInFlight = 2;
getEventHandler(instance, 'sending')(mockFile, mockXhr, { append: jest.fn() });

mockXhr.onload({});

expect(instance.chunksInFlight).toBe(2);
});

/**
* Test Cases 11-13: pollUploadStatus's three failure branches route through the
* file-level error channel (dropzone.emit('error', file, message)) instead of
* calling onError directly, so the row clears and the consumer is told exactly once.
*/
test('test_dropzone_poll_timeout_emits_a_string_message_and_calls_onError_once', async () => {
jest.useFakeTimers({ doNotFake: ['queueMicrotask'] });
global.fetch = jest.fn(() =>
Promise.resolve({ ok: true, json: () => Promise.resolve({ status: 'uploading' }) })
);

const ref = React.createRef();
render(
<DropzoneJS
{...defaultProps}
ref={ref}
onUploadComplete={onUploadCompleteMock}
onError={onErrorMock}
/>
);

const instance = ref.current;
const mockFile = { name: 'big.pdf', size: 1024000 };
instance.pollUploadStatus('file-timeout', 'https://example.com/upload', mockFile);

// maxAttempts is 300 at 2s/tick - advance one tick past the ceiling.
await jest.advanceTimersByTimeAsync(2000 * 301);

expect(onErrorMock).toHaveBeenCalledTimes(1);
const [message] = onErrorMock.mock.calls[0];
expect(typeof message).toBe('string');
expect(message).toBe('Upload timed out');

jest.useRealTimers();
}, 20000);

test('test_dropzone_poll_server_error_status_emits_readable_message_not_object', (done) => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ status: 'error', message: 'processing failed' })
})
);

const ref = React.createRef();
render(
<DropzoneJS
{...defaultProps}
ref={ref}
onUploadComplete={onUploadCompleteMock}
onError={onErrorMock}
/>
);

setTimeout(() => {
const instance = ref.current;
const mockFile = { name: 'test.pdf', size: 1024000 };
instance.pollUploadStatus('file-server-error', 'https://example.com/upload', mockFile);

setTimeout(() => {
expect(onErrorMock).toHaveBeenCalledTimes(1);
const [message] = onErrorMock.mock.calls[0];
expect(message).toBe('processing failed');
expect(message).not.toBe('[object Object]');
done();
}, 2500);
}, 10);
}, 10000);

/**
* Test Case 14: a slow status request must not let a second one start alongside it.
*
* The old setInterval-based poll fired every 2s regardless of whether the previous
* tick's fetch had resolved yet, so a slow response could still be in flight when a
* later tick's request resolved first - letting two terminal results race (e.g. a
* stray error landing after an already-committed completion). The self-scheduling loop
* only calls sleep(2000) again after its current request fully resolves, so no second
* request can ever start while one is outstanding - this asserts that directly, then
* confirms the single in-flight request still completes normally once it resolves.
*/
test('test_dropzone_poll_does_not_start_a_new_request_while_one_is_still_in_flight', async () => {
jest.useFakeTimers({ doNotFake: ['queueMicrotask'] });
let resolveFetch;
global.fetch = jest.fn(
() => new Promise((resolve) => { resolveFetch = resolve; })
);

const ref = React.createRef();
render(
<DropzoneJS
{...defaultProps}
ref={ref}
onUploadComplete={onUploadCompleteMock}
onError={onErrorMock}
/>
);

const instance = ref.current;
const mockFile = {
name: 'test.pdf',
size: 1024000,
_asyncProcessing: true,
_chunksUploadedDone: jest.fn()
};

instance.pollUploadStatus('file-123', 'https://example.com/upload', mockFile);

// First tick fires at 2000ms and parks on the still-unresolved request.
await jest.advanceTimersByTimeAsync(2000);
expect(global.fetch).toHaveBeenCalledTimes(1);

// Several more tick-intervals' worth of time pass while that request is still
// outstanding - a second request must not start.
await jest.advanceTimersByTimeAsync(2000 * 5);
expect(global.fetch).toHaveBeenCalledTimes(1);

// Now the one in-flight request finally resolves with a terminal result.
resolveFetch({
ok: true,
json: () => Promise.resolve({ status: 'complete', name: 'test.pdf', size: 1024000 })
});
await jest.advanceTimersByTimeAsync(0);

expect(mockFile._chunksUploadedDone).toHaveBeenCalledTimes(1);
expect(onUploadCompleteMock).toHaveBeenCalledTimes(1);
expect(onErrorMock).not.toHaveBeenCalled();

jest.useRealTimers();
}, 10000);

test('test_dropzone_poll_fetch_rejection_emits_readable_message_and_calls_onError_once', (done) => {
global.fetch = jest.fn(() => Promise.reject(new Error('network down')));

const ref = React.createRef();
render(
<DropzoneJS
{...defaultProps}
ref={ref}
onUploadComplete={onUploadCompleteMock}
onError={onErrorMock}
/>
);

setTimeout(() => {
const instance = ref.current;
const mockFile = { name: 'test.pdf', size: 1024000 };
instance.pollUploadStatus('file-network-error', 'https://example.com/upload', mockFile);

setTimeout(() => {
expect(onErrorMock).toHaveBeenCalledTimes(1);
const [message] = onErrorMock.mock.calls[0];
expect(message).toBe('Network error');
done();
}, 2500);
}, 10);
}, 10000);
});

describe('DropzoneJS - Progress Bar Monotonicity', () => {
Expand Down
Loading
Loading