Release 2.21.3: multi-file media uploads
This commit is contained in:
1 parent
24a566a1a4
commit
75b01d49a9
8 files changed
+131
-74
No files matched your search
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## 2.21.3 - 2026-10-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Media library uploads accept multiple files at once, show per-file progress, and continue the batch when an individual file fails.
|
||||||
|
|
||||||
## 2.21.2 - 2026-10-26
|
## 2.21.2 - 2026-10-26
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-player",
|
"name": "pulse-signage-player",
|
||||||
"version": "2.21.2",
|
"version": "2.21.3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage player application bundle",
|
"description": "Pulse Signage player application bundle",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-web",
|
"name": "pulse-signage-web",
|
||||||
"version": "2.21.2",
|
"version": "2.21.3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage web and bridge application bundle",
|
"description": "Pulse Signage web and bridge application bundle",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.21.2",
|
"version": "2.21.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.21.2",
|
"version": "2.21.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@better-auth/passkey": "^1.7.13",
|
"@better-auth/passkey": "^1.7.13",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.21.2",
|
"version": "2.21.3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -146,32 +146,43 @@ if (addMediaButton && uploadModal) {
|
|||||||
column.querySelector('.media-gallery-card').classList.toggle('media-gallery-card-selected', selectedAssetIds.has(String(asset.id || '')));
|
column.querySelector('.media-gallery-card').classList.toggle('media-gallery-card-selected', selectedAssetIds.has(String(asset.id || '')));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setUploadFile(file) {
|
function getUploadFileError(file) {
|
||||||
if (!uploadInput || uploadInput.disabled || !file) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const allowedTypes = String(uploadInput.accept || '').split(',').map(function (value) { return value.trim().toLowerCase(); }).filter(Boolean);
|
const allowedTypes = String(uploadInput.accept || '').split(',').map(function (value) { return value.trim().toLowerCase(); }).filter(Boolean);
|
||||||
const fileType = String(file.type || '').toLowerCase();
|
const fileType = String(file.type || '').toLowerCase();
|
||||||
const matchesType = allowedTypes.length === 0 || allowedTypes.indexOf(fileType) !== -1;
|
const matchesType = allowedTypes.length === 0 || allowedTypes.indexOf(fileType) !== -1;
|
||||||
const maxBytes = fileType.indexOf('video/') === 0 ? Number(uploadInput.dataset.videoMaxBytes) : Number(uploadInput.dataset.imageMaxBytes);
|
const maxBytes = fileType.indexOf('video/') === 0 ? Number(uploadInput.dataset.videoMaxBytes) : Number(uploadInput.dataset.imageMaxBytes);
|
||||||
if (!matchesType) {
|
if (!matchesType) {
|
||||||
window.PulseUpload.showMessage('This media type is disabled in Media Uploads settings.');
|
return 'This media type is disabled in Media Uploads settings.';
|
||||||
uploadInput.value = '';
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
if (maxBytes && file.size > maxBytes) {
|
if (maxBytes && file.size > maxBytes) {
|
||||||
window.PulseUpload.showMessage('This file is larger than the current upload limit of ' + window.PulseUpload.formatUploadLimit(maxBytes) + '.');
|
return file.name + ' is larger than the current upload limit of ' + window.PulseUpload.formatUploadLimit(maxBytes) + '.';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUploadFiles(files) {
|
||||||
|
if (!uploadInput || uploadInput.disabled || !files || !files.length) return false;
|
||||||
|
const validFiles = [];
|
||||||
|
const errors = [];
|
||||||
|
Array.from(files).forEach(function (file) {
|
||||||
|
const error = getUploadFileError(file);
|
||||||
|
if (error) errors.push(error);
|
||||||
|
else validFiles.push(file);
|
||||||
|
});
|
||||||
|
if (errors.length) {
|
||||||
|
const uniqueErrors = Array.from(new Set(errors));
|
||||||
|
window.PulseUpload.showMessage((errors.length === 1 ? '' : errors.length + ' files skipped. ') + uniqueErrors.slice(0, 3).join(' '));
|
||||||
|
}
|
||||||
|
if (!validFiles.length) {
|
||||||
uploadInput.value = '';
|
uploadInput.value = '';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const transfer = new DataTransfer();
|
const transfer = new DataTransfer();
|
||||||
transfer.items.add(file);
|
validFiles.forEach(function (file) { transfer.items.add(file); });
|
||||||
uploadInput.files = transfer.files;
|
uploadInput.files = transfer.files;
|
||||||
if (uploadName) {
|
if (uploadName) {
|
||||||
uploadName.textContent = file.name;
|
uploadName.textContent = validFiles.length === 1 ? validFiles[0].name : validFiles.length + ' files selected';
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,8 +199,8 @@ function submitUpload() {
|
|||||||
|
|
||||||
if (uploadInput) {
|
if (uploadInput) {
|
||||||
uploadInput.addEventListener('change', function () {
|
uploadInput.addEventListener('change', function () {
|
||||||
const file = uploadInput.files && uploadInput.files[0];
|
const files = uploadInput.files;
|
||||||
if (file && setUploadFile(file)) {
|
if (files && setUploadFiles(files)) {
|
||||||
submitUpload();
|
submitUpload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -206,8 +217,8 @@ if (uploadZone) {
|
|||||||
uploadZone.addEventListener('drop', function (event) {
|
uploadZone.addEventListener('drop', function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
uploadZone.classList.remove('is-dragover');
|
uploadZone.classList.remove('is-dragover');
|
||||||
const file = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0];
|
const files = event.dataTransfer && event.dataTransfer.files;
|
||||||
if (file && setUploadFile(file)) {
|
if (files && setUploadFiles(files)) {
|
||||||
submitUpload();
|
submitUpload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -238,17 +249,16 @@ if (uploadForm) {
|
|||||||
uploadForm.addEventListener('submit', function (event) {
|
uploadForm.addEventListener('submit', function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (uploadInProgress) return;
|
if (uploadInProgress) return;
|
||||||
if (uploadInput && !uploadInput.files.length) {
|
const files = uploadInput ? Array.from(uploadInput.files || []) : [];
|
||||||
|
if (!files.length) {
|
||||||
window.PulseUpload.showMessage('Choose an image or video to upload.');
|
window.PulseUpload.showMessage('Choose an image or video to upload.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const formData = new FormData(uploadForm);
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
const buttons = uploadModal ? Array.from(uploadModal.querySelectorAll('button')).map(button => ({ button, disabled: button.disabled })) : [];
|
const buttons = uploadModal ? Array.from(uploadModal.querySelectorAll('button')).map(button => ({ button, disabled: button.disabled })) : [];
|
||||||
uploadInProgress = true;
|
uploadInProgress = true;
|
||||||
if (uploadInput) uploadInput.disabled = true;
|
if (uploadInput) uploadInput.disabled = true;
|
||||||
buttons.forEach(state => { state.button.disabled = true; });
|
buttons.forEach(state => { state.button.disabled = true; });
|
||||||
setUploadProgress(0, 'Uploading...', true);
|
setUploadProgress(0, 'Uploading 1 of ' + files.length + ': ' + files[0].name, true);
|
||||||
|
|
||||||
function finish() {
|
function finish() {
|
||||||
uploadInProgress = false;
|
uploadInProgress = false;
|
||||||
@@ -256,35 +266,66 @@ if (uploadForm) {
|
|||||||
buttons.forEach(state => { state.button.disabled = state.disabled; });
|
buttons.forEach(state => { state.button.disabled = state.disabled; });
|
||||||
setUploadProgress(0, '', false);
|
setUploadProgress(0, '', false);
|
||||||
}
|
}
|
||||||
function fail(message) {
|
|
||||||
finish();
|
function uploadFile(file, index) {
|
||||||
window.PulseUpload.showMessage(message);
|
return new Promise(function (resolve) {
|
||||||
}
|
const xhr = new XMLHttpRequest();
|
||||||
|
const fileLabel = (index + 1) + ' of ' + files.length + ': ' + file.name;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file, file.name);
|
||||||
xhr.open('POST', uploadForm.action, true);
|
xhr.open('POST', uploadForm.action, true);
|
||||||
xhr.withCredentials = true;
|
xhr.withCredentials = true;
|
||||||
xhr.timeout = 300000;
|
xhr.timeout = 300000;
|
||||||
xhr.setRequestHeader('Accept', 'application/json');
|
xhr.setRequestHeader('Accept', 'application/json');
|
||||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||||
xhr.upload.onprogress = function (event) {
|
xhr.upload.onprogress = function (progressEvent) {
|
||||||
if (!event.lengthComputable || !event.total) { setUploadProgress(100, 'Uploading...', true); return; }
|
if (!progressEvent.lengthComputable || !progressEvent.total) { setUploadProgress(100, 'Uploading ' + fileLabel, true); return; }
|
||||||
const percent = Math.min(100, Math.round(event.loaded / event.total * 100));
|
const percent = Math.min(100, Math.round(progressEvent.loaded / progressEvent.total * 100));
|
||||||
setUploadProgress(percent, percent === 100 ? 'Processing...' : 'Uploading ' + percent + '%', percent === 100);
|
setUploadProgress(percent, (percent === 100 ? 'Processing ' : 'Uploading ') + fileLabel + (percent === 100 ? '' : ' (' + percent + '%)'), percent === 100);
|
||||||
};
|
};
|
||||||
xhr.upload.onload = function () { setUploadProgress(100, 'Processing...', true); };
|
xhr.upload.onload = function () { setUploadProgress(100, 'Processing ' + fileLabel, true); };
|
||||||
xhr.onload = async function () {
|
xhr.onload = function () {
|
||||||
let result;
|
let result;
|
||||||
try { result = JSON.parse(xhr.responseText || '{}'); } catch (_error) { fail('Unable to upload media. Check your sign-in status and retry.'); return; }
|
try { result = JSON.parse(xhr.responseText || '{}'); } catch (_error) {
|
||||||
if (xhr.status < 200 || xhr.status >= 300 || !result.success) { fail(result.error || 'Unable to upload media.'); return; }
|
resolve({ success: false, error: 'Unable to upload. Check your sign-in status and retry.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(xhr.status >= 200 && xhr.status < 300 && result.success
|
||||||
|
? { success: true }
|
||||||
|
: { success: false, error: result.error || 'Unable to upload.' });
|
||||||
|
};
|
||||||
|
xhr.onerror = function () { resolve({ success: false, error: 'Unable to upload. Check your connection and retry.' }); };
|
||||||
|
xhr.onabort = function () { resolve({ success: false, error: 'Upload cancelled.' }); };
|
||||||
|
xhr.ontimeout = function () { resolve({ success: false, error: 'Upload timed out.' }); };
|
||||||
|
try { xhr.send(formData); } catch (_error) { resolve({ success: false, error: 'Unable to upload. Please try again.' }); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function () {
|
||||||
|
const succeeded = [];
|
||||||
|
const failed = [];
|
||||||
|
for (let index = 0; index < files.length; index += 1) {
|
||||||
|
const result = await uploadFile(files[index], index);
|
||||||
|
if (result.success) succeeded.push(files[index]);
|
||||||
|
else failed.push({ file: files[index], error: result.error });
|
||||||
|
}
|
||||||
finish();
|
finish();
|
||||||
if (uploadInput) uploadInput.value = '';
|
if (uploadInput) uploadInput.value = '';
|
||||||
hideModal(uploadModal);
|
hideModal(uploadModal);
|
||||||
if (typeof window.showToast === 'function') window.showToast('Media uploaded.', 'success');
|
if (succeeded.length && typeof window.showToast === 'function') {
|
||||||
await refreshGallery();
|
const message = succeeded.length === 1 ? 'Media uploaded.' : succeeded.length + ' media files uploaded.';
|
||||||
};
|
window.showToast(message, 'success');
|
||||||
xhr.onerror = function () { fail('Unable to upload media. Check your connection and retry.'); };
|
}
|
||||||
xhr.onabort = function () { fail('Upload cancelled.'); };
|
if (failed.length) {
|
||||||
xhr.ontimeout = function () { fail('Upload timed out. Please try again.'); };
|
const details = failed.slice(0, 3).map(function (item) { return item.file.name + ': ' + item.error; });
|
||||||
try { xhr.send(formData); } catch (_error) { fail('Unable to upload media. Please try again.'); }
|
if (failed.length > details.length) details.push((failed.length - details.length) + ' more failed.');
|
||||||
|
window.PulseUpload.showMessage((succeeded.length ? succeeded.length + ' uploaded. ' : 'No media files were uploaded. ') + details.join(' '));
|
||||||
|
}
|
||||||
|
if (succeeded.length) await refreshGallery();
|
||||||
|
})().catch(function () {
|
||||||
|
finish();
|
||||||
|
window.PulseUpload.showMessage('Unable to finish uploading media. Please refresh the library and check the results.');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,11 +115,11 @@
|
|||||||
<form method="post" action="/media-library" enctype="multipart/form-data">
|
<form method="post" action="/media-library" enctype="multipart/form-data">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<label class="slide-image-region-upload-zone media-library-upload-zone" for="media-library-upload" data-media-library-upload-zone>
|
<label class="slide-image-region-upload-zone media-library-upload-zone" for="media-library-upload" data-media-library-upload-zone>
|
||||||
<input type="file" id="media-library-upload" name="file" class="visually-hidden" accept="{{#each uploadLimits.allowedMimeTypes}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}" data-media-library-upload data-image-max-bytes="{{uploadLimits.imageMaxBytes}}" data-video-max-bytes="{{uploadLimits.videoMaxBytes}}" required />
|
<input type="file" id="media-library-upload" name="file" class="visually-hidden" accept="{{#each uploadLimits.allowedMimeTypes}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}" data-media-library-upload data-image-max-bytes="{{uploadLimits.imageMaxBytes}}" data-video-max-bytes="{{uploadLimits.videoMaxBytes}}" multiple required />
|
||||||
<span class="slide-image-region-upload-zone-content">
|
<span class="slide-image-region-upload-zone-content">
|
||||||
<span class="slide-image-region-upload-zone-icon"><i class="bi bi-cloud-arrow-up" aria-hidden="true"></i></span>
|
<span class="slide-image-region-upload-zone-icon"><i class="bi bi-cloud-arrow-up" aria-hidden="true"></i></span>
|
||||||
<span class="slide-image-region-upload-zone-copy">
|
<span class="slide-image-region-upload-zone-copy">
|
||||||
<strong>Drop an image or video here or click to upload</strong>
|
<strong>Drop images or videos here or click to select files</strong>
|
||||||
<span>Images and videos enabled in Media Uploads settings</span>
|
<span>Images and videos enabled in Media Uploads settings</span>
|
||||||
<span class="slide-image-region-upload-zone-limit">Images max {{uploadLimits.imageMaxLabel}}; videos max {{uploadLimits.videoMaxLabel}}</span>
|
<span class="slide-image-region-upload-zone-limit">Images max {{uploadLimits.imageMaxLabel}}; videos max {{uploadLimits.videoMaxLabel}}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+38
-28
@@ -504,11 +504,11 @@ test('crop loading toggles the frame overlay and accessible busy state', () => {
|
|||||||
assert.match(source, /addEventListener\('error', function \(\) \{\s+setLoading\(false\);/);
|
assert.match(source, /addEventListener\('error', function \(\) \{\s+setLoading\(false\);/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('library upload selection and drop submit the original File and reject invalid files', () => {
|
test('library upload selection and drop accept valid batches and skip invalid files', () => {
|
||||||
const source = fs.readFileSync(path.join(__dirname, '../src/web/public/js/settings/media-library.js'), 'utf8');
|
const source = fs.readFileSync(path.join(__dirname, '../src/web/public/js/settings/media-library.js'), 'utf8');
|
||||||
const handlers = {};
|
const handlers = {};
|
||||||
let submissions = 0;
|
let submissions = 0;
|
||||||
const input = { accept: 'image/png, image/gif, image/svg+xml', dataset: { imageMaxBytes: '1000' }, files: [], addEventListener(name, handler) { handlers[name] = handler; } };
|
const input = { accept: 'image/png, image/gif, image/svg+xml', dataset: { imageMaxBytes: '1000' }, files: [], value: '', addEventListener(name, handler) { handlers[name] = handler; } };
|
||||||
const zone = { classList: { add() {}, remove() {} }, addEventListener(name, handler) { handlers[name] = handler; } };
|
const zone = { classList: { add() {}, remove() {} }, addEventListener(name, handler) { handlers[name] = handler; } };
|
||||||
const context = vm.createContext({
|
const context = vm.createContext({
|
||||||
uploadInput: input, uploadZone: zone, uploadName: {},
|
uploadInput: input, uploadZone: zone, uploadName: {},
|
||||||
@@ -516,20 +516,21 @@ test('library upload selection and drop submit the original File and reject inva
|
|||||||
window: { PulseUpload: { showMessage() {}, formatUploadLimit: String } },
|
window: { PulseUpload: { showMessage() {}, formatUploadLimit: String } },
|
||||||
DataTransfer: class { constructor() { this.files = []; this.items = { add: file => this.files.push(file) }; } }
|
DataTransfer: class { constructor() { this.files = []; this.items = { add: file => this.files.push(file) }; } }
|
||||||
});
|
});
|
||||||
vm.runInContext(source.slice(source.indexOf('function setUploadFile('), source.indexOf('if (uploadForm) {')), context);
|
vm.runInContext(source.slice(source.indexOf('function getUploadFileError('), source.indexOf('if (uploadForm) {')), context);
|
||||||
for (const type of ['image/png', 'image/gif', 'image/svg+xml']) {
|
const first = { name: 'first.png', type: 'image/png', size: 42 };
|
||||||
const file = { name: 'original', type, size: 42 };
|
const second = { name: 'second.gif', type: 'image/gif', size: 42 };
|
||||||
input.files = [file];
|
const tooLarge = { name: 'large.png', type: 'image/png', size: 1001 };
|
||||||
|
const unsupported = { name: 'document.txt', type: 'text/plain', size: 1 };
|
||||||
|
input.files = [first, second, tooLarge];
|
||||||
handlers.change();
|
handlers.change();
|
||||||
assert.equal(input.files[0], file);
|
assert.deepEqual(Array.from(input.files), [first, second]);
|
||||||
handlers.drop({ preventDefault() {}, dataTransfer: { files: [file] } });
|
assert.equal(submissions, 1);
|
||||||
assert.equal(input.files[0], file);
|
handlers.drop({ preventDefault() {}, dataTransfer: { files: [first, second] } });
|
||||||
}
|
assert.deepEqual(Array.from(input.files), [first, second]);
|
||||||
assert.equal(submissions, 6);
|
assert.equal(submissions, 2);
|
||||||
input.files = [{ type: 'image/png', size: 1001 }];
|
input.files = [unsupported];
|
||||||
handlers.change();
|
handlers.change();
|
||||||
handlers.drop({ preventDefault() {}, dataTransfer: { files: [{ type: 'text/plain', size: 1 }] } });
|
assert.equal(submissions, 2);
|
||||||
assert.equal(submissions, 6);
|
|
||||||
const slideForm = fs.readFileSync(path.join(__dirname, '../src/web/views/signage/slides/form.hbs'), 'utf8');
|
const slideForm = fs.readFileSync(path.join(__dirname, '../src/web/views/signage/slides/form.hbs'), 'utf8');
|
||||||
assert.doesNotMatch(slideForm, /slide-image-cropper|cropper\.min/);
|
assert.doesNotMatch(slideForm, /slide-image-cropper|cropper\.min/);
|
||||||
});
|
});
|
||||||
@@ -826,7 +827,7 @@ test('media library upload and delete routes audit successful asset changes', as
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('media library drop-zone upload reports transfer progress, processing, errors and success', async () => {
|
test('media library uploads a batch sequentially and reports independent failures', async () => {
|
||||||
const source = fs.readFileSync(path.join(__dirname, '../src/web/public/js/settings/media-library.js'), 'utf8');
|
const source = fs.readFileSync(path.join(__dirname, '../src/web/public/js/settings/media-library.js'), 'utf8');
|
||||||
const handlers = {};
|
const handlers = {};
|
||||||
const requests = [];
|
const requests = [];
|
||||||
@@ -836,7 +837,7 @@ test('media library drop-zone upload reports transfer progress, processing, erro
|
|||||||
const label = { textContent: '' };
|
const label = { textContent: '' };
|
||||||
const bar = { setAttribute(name, value) { attributes[name] = value; }, removeAttribute(name) { delete attributes[name]; } };
|
const bar = { setAttribute(name, value) { attributes[name] = value; }, removeAttribute(name) { delete attributes[name]; } };
|
||||||
const fill = { style: {}, classList: { toggle() {} }, textContent: '' };
|
const fill = { style: {}, classList: { toggle() {} }, textContent: '' };
|
||||||
const input = { disabled: false, files: [{ name: 'original.png' }], value: 'original.png' };
|
const input = { disabled: false, files: [{ name: 'first.png' }, { name: 'second.png' }, { name: 'third.png' }], value: 'selected' };
|
||||||
const closeButton = { disabled: false };
|
const closeButton = { disabled: false };
|
||||||
let refreshed = 0;
|
let refreshed = 0;
|
||||||
let hidden = 0;
|
let hidden = 0;
|
||||||
@@ -851,7 +852,7 @@ test('media library drop-zone upload reports transfer progress, processing, erro
|
|||||||
uploadModal: { querySelectorAll() { return [closeButton]; }, addEventListener(name, handler) { handlers[name] = handler; } },
|
uploadModal: { querySelectorAll() { return [closeButton]; }, addEventListener(name, handler) { handlers[name] = handler; } },
|
||||||
window: { PulseUpload: { showMessage(message) { messages.push(message); } }, showToast() {} },
|
window: { PulseUpload: { showMessage(message) { messages.push(message); } }, showToast() {} },
|
||||||
hideModal() { hidden += 1; }, async refreshGallery() { refreshed += 1; },
|
hideModal() { hidden += 1; }, async refreshGallery() { refreshed += 1; },
|
||||||
FormData: class { constructor() { assert.equal(input.disabled, false); } },
|
FormData: class { constructor() { assert.equal(input.disabled, true); this.files = []; } append(name, file) { this.files.push({ name, file }); } },
|
||||||
XMLHttpRequest: class {
|
XMLHttpRequest: class {
|
||||||
constructor() { this.upload = {}; requests.push(this); }
|
constructor() { this.upload = {}; requests.push(this); }
|
||||||
open(method, url) { this.method = method; this.url = url; }
|
open(method, url) { this.method = method; this.url = url; }
|
||||||
@@ -868,12 +869,14 @@ test('media library drop-zone upload reports transfer progress, processing, erro
|
|||||||
assert.equal(input.disabled, true);
|
assert.equal(input.disabled, true);
|
||||||
assert.equal(closeButton.disabled, true);
|
assert.equal(closeButton.disabled, true);
|
||||||
assert.equal(attributes['aria-busy'], 'true');
|
assert.equal(attributes['aria-busy'], 'true');
|
||||||
|
assert.equal(requests.length, 1);
|
||||||
|
assert.equal(requests[0].data.files[0].file.name, 'first.png');
|
||||||
requests[0].upload.onprogress({ lengthComputable: true, loaded: 250, total: 1000 });
|
requests[0].upload.onprogress({ lengthComputable: true, loaded: 250, total: 1000 });
|
||||||
assert.equal(fill.style.width, '25%');
|
assert.equal(fill.style.width, '25%');
|
||||||
assert.equal(label.textContent, 'Uploading 25%');
|
assert.equal(label.textContent, 'Uploading 1 of 3: first.png (25%)');
|
||||||
assert.equal(attributes['aria-valuenow'], '25');
|
assert.equal(attributes['aria-valuenow'], '25');
|
||||||
requests[0].upload.onload();
|
requests[0].upload.onload();
|
||||||
assert.equal(label.textContent, 'Processing...');
|
assert.equal(label.textContent, 'Processing 1 of 3: first.png');
|
||||||
assert.equal(fill.style.width, '100%');
|
assert.equal(fill.style.width, '100%');
|
||||||
assert.equal(attributes['aria-valuenow'], undefined);
|
assert.equal(attributes['aria-valuenow'], undefined);
|
||||||
submit();
|
submit();
|
||||||
@@ -884,24 +887,31 @@ test('media library drop-zone upload reports transfer progress, processing, erro
|
|||||||
requests[0].status = 400;
|
requests[0].status = 400;
|
||||||
requests[0].responseText = JSON.stringify({ error: 'Upload rejected.' });
|
requests[0].responseText = JSON.stringify({ error: 'Upload rejected.' });
|
||||||
await requests[0].onload();
|
await requests[0].onload();
|
||||||
assert.deepEqual(messages, ['Upload rejected.']);
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
assert.equal(requests.length, 2);
|
||||||
|
assert.equal(requests[1].data.files[0].file.name, 'second.png');
|
||||||
|
requests[1].status = 200;
|
||||||
|
requests[1].responseText = JSON.stringify({ success: true });
|
||||||
|
await requests[1].onload();
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
assert.equal(requests.length, 3);
|
||||||
|
assert.equal(requests[2].data.files[0].file.name, 'third.png');
|
||||||
|
requests[2].ontimeout();
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
assert.deepEqual(messages, ['1 uploaded. first.png: Upload rejected. third.png: Upload timed out.']);
|
||||||
assert.equal(input.disabled, false);
|
assert.equal(input.disabled, false);
|
||||||
assert.equal(closeButton.disabled, false);
|
assert.equal(closeButton.disabled, false);
|
||||||
assert.equal(progress.hidden, true);
|
assert.equal(progress.hidden, true);
|
||||||
submit();
|
|
||||||
requests[1].ontimeout();
|
|
||||||
assert.equal(input.disabled, false);
|
|
||||||
assert.match(messages[1], /timed out/);
|
|
||||||
submit();
|
|
||||||
requests[2].status = 200;
|
|
||||||
requests[2].responseText = JSON.stringify({ success: true });
|
|
||||||
await requests[2].onload();
|
|
||||||
assert.equal(hidden, 1);
|
assert.equal(hidden, 1);
|
||||||
assert.equal(refreshed, 1);
|
assert.equal(refreshed, 1);
|
||||||
assert.equal(input.value, '');
|
assert.equal(input.value, '');
|
||||||
assert.equal(progress.hidden, true);
|
assert.equal(progress.hidden, true);
|
||||||
assert.equal(attributes['aria-busy'], 'false');
|
assert.equal(attributes['aria-busy'], 'false');
|
||||||
|
assert.deepEqual(requests.map(request => request.method), ['POST', 'POST', 'POST']);
|
||||||
|
assert.ok(requests.every(request => request.url === '/media-library'));
|
||||||
|
assert.equal(requests[1].withCredentials, true);
|
||||||
const view = fs.readFileSync(path.join(__dirname, '../src/web/views/settings/media-library/list.hbs'), 'utf8');
|
const view = fs.readFileSync(path.join(__dirname, '../src/web/views/settings/media-library/list.hbs'), 'utf8');
|
||||||
|
assert.match(view, /data-media-library-upload[^>]*multiple/);
|
||||||
assert.match(view, /data-media-library-upload-progress/);
|
assert.match(view, /data-media-library-upload-progress/);
|
||||||
assert.match(view, /role="progressbar" aria-label="Media upload progress"/);
|
assert.match(view, /role="progressbar" aria-label="Media upload progress"/);
|
||||||
assert.match(view, /class="progress rounded-pill slide-image-region-upload-zone-progress-bar"/);
|
assert.match(view, /class="progress rounded-pill slide-image-region-upload-zone-progress-bar"/);
|
||||||
|
|||||||
Reference in new issue
Block a user