diff --git a/lib/components/app.vue b/lib/components/app.vue
index 5c8f0312b..855fda807 100644
--- a/lib/components/app.vue
+++ b/lib/components/app.vue
@@ -176,24 +176,24 @@ h1 {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}
-.no-text-decoration {
+.text-no-decoration {
&, &:hover, &:focus, &.focus {
text-decoration: none;
}
}
-@media print {
- a.no-text-decoration:visited {
- text-decoration: none;
+a:hover, a:focus {
+ .underline-within-link {
+ text-decoration: underline;
}
}
-.underline-on-hover-or-focus {
- &:hover, &:focus {
- text-decoration: underline;
+@media print {
+ a.text-no-decoration:visited {
+ text-decoration: none;
}
- @media print {
+ .underline-within-link {
text-decoration: underline;
}
}
diff --git a/lib/components/field-key/row.vue b/lib/components/field-key/row.vue
index 25bee9787..b9546308e 100644
--- a/lib/components/field-key/row.vue
+++ b/lib/components/field-key/row.vue
@@ -15,11 +15,11 @@ except according to the terms contained in the LICENSE file.
{{ created }}
{{ lastUsed }}
-
- See code
+ See code
Access revoked
diff --git a/lib/components/form/attachment/list.vue b/lib/components/form/attachment/list.vue
index 31f0a2ceb..54d04ebbd 100644
--- a/lib/components/form/attachment/list.vue
+++ b/lib/components/form/attachment/list.vue
@@ -47,7 +47,7 @@ except according to the terms contained in the LICENSE file.
@confirm="uploadFiles" @cancel="cancelUploads"/>
+ @hide="hideModal('uploadFilesModal')" @choose="afterChoose"/>
@@ -152,6 +152,17 @@ export default {
}
},
methods: {
+ ////////////////////////////////////////////////////////////////////////////
+ // File picker
+
+ afterChoose(files) {
+ this.uploadFilesModal.state = false;
+ this.matchFilesToAttachments(files);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Drag and drop
+
// items is a DataTransferItemList, not an Array.
fileItemCount(maybeItems) {
// IE
@@ -180,6 +191,50 @@ export default {
this.countOfFilesOverDropZone = 0;
}
},
+ ondrop(jQueryEvent) {
+ this.countOfFilesOverDropZone = 0;
+ const { files } = jQueryEvent.originalEvent.dataTransfer;
+ if (this.dragoverAttachment != null) {
+ const upload = { attachment: this.dragoverAttachment, file: files[0] };
+ this.dragoverAttachment = null;
+ this.plannedUploads.push(upload);
+ if (upload.file.name === upload.attachment.name)
+ this.uploadFiles();
+ else
+ this.showModal('nameMismatch');
+ } else {
+ // The else case can be reached even if this.countOfFilesOverDropZone
+ // was 1, if the drop was not over a row.
+ this.matchFilesToAttachments(files);
+ }
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Core logic
+
+ matchFilesToAttachments(files) {
+ this.plannedUploads = [];
+ this.unmatchedFiles = [];
+ // files is a FileList, not an Array, hence the style of for-loop.
+ for (let i = 0; i < files.length; i += 1) {
+ const file = files[i];
+ const attachment = this.attachments.find(a => a.name === file.name);
+ if (attachment != null)
+ this.plannedUploads.push({ attachment, file });
+ else
+ this.unmatchedFiles.push(file);
+ }
+ // With the changes to this.plannedUploads and this.unmatchedFiles, the
+ // popup will show in the next tick.
+ },
+ // cancelUploads() cancels the uploads before they start, after files have
+ // been dropped. (It does not cancel an upload in progress.)
+ cancelUploads() {
+ // Checking `length` in order to avoid setting these properties
+ // unnecessarily, which could result in Vue calculations.
+ if (this.plannedUploads.length !== 0) this.plannedUploads = [];
+ if (this.unmatchedFiles.length !== 0) this.unmatchedFiles = [];
+ },
problemToAlert(problem) {
if (this.uploadStatus.total === 1) return null;
const uploaded = this.uploadStatus.total - this.uploadStatus.remaining;
@@ -229,43 +284,6 @@ export default {
.catch(() => {});
this.plannedUploads = [];
this.unmatchedFiles = [];
- },
- ondrop(jQueryEvent) {
- this.countOfFilesOverDropZone = 0;
- const { files } = jQueryEvent.originalEvent.dataTransfer;
- if (this.dragoverAttachment != null) {
- const upload = { attachment: this.dragoverAttachment, file: files[0] };
- this.dragoverAttachment = null;
- this.plannedUploads.push(upload);
- if (upload.file.name === upload.attachment.name)
- this.uploadFiles();
- else
- this.showModal('nameMismatch');
- } else {
- // The else case can be reached even if this.countOfFilesOverDropZone
- // was 1, if the drop was not over a row.
-
- // files is a FileList, not an Array, hence the style of for-loop.
- for (let i = 0; i < files.length; i += 1) {
- const file = files[i];
- const attachment = this.attachments.find(a => a.name === file.name);
- if (attachment != null)
- this.plannedUploads.push({ attachment, file });
- else
- this.unmatchedFiles.push(file);
- }
-
- // With the changes to this.plannedUploads and this.unmatchedFiles, the
- // popup will show in the next tick.
- }
- },
- // cancelUploads() cancels the uploads before they start, after files have
- // been dropped. (It does not cancel an upload in progress.)
- cancelUploads() {
- // Checking `length` in order to avoid setting these properties
- // unnecessarily, which could result in Vue calculations.
- if (this.plannedUploads.length !== 0) this.plannedUploads = [];
- if (this.unmatchedFiles.length !== 0) this.unmatchedFiles = [];
}
}
};
diff --git a/lib/components/form/attachment/popups.vue b/lib/components/form/attachment/popups.vue
index 098f1307b..142b6e9a9 100644
--- a/lib/components/form/attachment/popups.vue
+++ b/lib/components/form/attachment/popups.vue
@@ -56,7 +56,7 @@ except according to the terms contained in the LICENSE file.
-
Looks good, proceed
@@ -149,6 +149,10 @@ export default {
// in IE 10.
return `${Math.floor(100 * (loaded / total)).toLocaleString()}%`;
}
+ },
+ updated() {
+ if (this.plannedUploads.length !== 0 && !this.nameMismatch.state)
+ this.$refs.confirmButton.focus();
}
};
diff --git a/lib/components/form/attachment/upload-files.vue b/lib/components/form/attachment/upload-files.vue
index 2fbad614c..59bd8832a 100644
--- a/lib/components/form/attachment/upload-files.vue
+++ b/lib/components/form/attachment/upload-files.vue
@@ -10,11 +10,22 @@ including this file, may be copied, modified, propagated, or distributed
except according to the terms contained in the LICENSE file.
-->
-
+
Upload Files
- To upload files, please drag and drop files onto the table on this page.
+ To upload files, you can drag and drop one or more
+ files onto the table on this page.
+
+
+ If you would rather select files from a prompt, ensure that their names
+ match the ones in the table and then
+
+
+
+ click here to choose .
@@ -33,6 +44,29 @@ export default {
type: Boolean,
default: false
}
+ },
+ mounted() {
+ // Using a jQuery event handler rather than a Vue one in order to facilitate
+ // testing: it is possible to mock a jQuery event but not a Vue event.
+ $(this.$refs.input).on('change.form-attachment-upload-files', (event) =>
+ this.$emit('choose', event.target.files));
+ },
+ beforeDestroy() {
+ $(this.$refs.input).off('.form-attachment-upload-files');
+ },
+ methods: {
+ focusLink() {
+ $(this.$refs.link).focus();
+ },
+ clickInput() {
+ $(this.$refs.input).click();
+ }
}
};
+
+
diff --git a/lib/components/form/new.vue b/lib/components/form/new.vue
index ba49ae0bf..d18e08ded 100644
--- a/lib/components/form/new.vue
+++ b/lib/components/form/new.vue
@@ -94,6 +94,8 @@ export default {
}
},
mounted() {
+ // Using a jQuery event handler rather than a Vue one in order to facilitate
+ // testing: it is possible to mock a jQuery event but not a Vue event.
$(this.$refs.input)
.on('change.form-new', (event) => this.readFile(event.target.files));
},
diff --git a/lib/components/form/submission/row.vue b/lib/components/form/submission/row.vue
index addcbbf4f..042948e05 100644
--- a/lib/components/form/submission/row.vue
+++ b/lib/components/form/submission/row.vue
@@ -23,7 +23,7 @@ except according to the terms contained in the LICENSE file.
:title="hasTitle(column) ? fieldValue(column) : null">
diff --git a/test/components/form/attachment/list.spec.js b/test/components/form/attachment/list.spec.js
index d3e5c82fa..4a10c0371 100644
--- a/test/components/form/attachment/list.spec.js
+++ b/test/components/form/attachment/list.spec.js
@@ -4,10 +4,10 @@ import FormAttachmentList from '../../../../lib/components/form/attachment/list.
import FormAttachmentNameMismatch from '../../../../lib/components/form/attachment/name-mismatch.vue';
import FormAttachmentUploadFiles from '../../../../lib/components/form/attachment/upload-files.vue';
import testData from '../../../data';
+import { dataTransfer, trigger } from '../../../event';
import { formatDate } from '../../../../lib/util';
import { mockHttp, mockRoute } from '../../../http';
import { mockLogin, mockRouteThroughLogin } from '../../../session';
-import { trigger } from '../../../event';
const form = () => testData.extendedForms.firstOrCreatePast();
const overviewPath = () => `/forms/${encodeURIComponent(form().xmlFormId)}`;
@@ -15,12 +15,13 @@ const mediaFilesPath = () => {
const encodedId = encodeURIComponent(form().xmlFormId);
return `/forms/${encodedId}/media-files`;
};
-const loadAttachments = ({ route = false } = {}) => {
+const loadAttachments = ({ route = false, attachToDocument = false } = {}) => {
if (route) {
- return mockRoute(mediaFilesPath())
+ return mockRoute(mediaFilesPath(), { attachToDocument })
.respondWithData(form)
.respondWithData(() => testData.extendedFormAttachments.sorted());
}
+ if (attachToDocument) throw new Error('invalid options');
return mockHttp()
.mount(FormAttachmentList, {
propsData: {
@@ -104,28 +105,6 @@ describe('FormAttachmentList', () => {
});
});
- describe('upload files modal', () => {
- beforeEach(mockLogin);
- beforeEach(() => {
- testData.extendedFormAttachments.createPast(1);
- });
-
- it('is initially hidden', () =>
- loadAttachments().then(component => {
- const modal = component.first(FormAttachmentUploadFiles);
- modal.getProp('state').should.be.false();
- }));
-
- it('is shown after button click', () =>
- loadAttachments()
- .then(component =>
- trigger.click(component, '#form-attachment-list-heading button'))
- .then(component => {
- const modal = component.first(FormAttachmentUploadFiles);
- modal.getProp('state').should.be.true();
- }));
- });
-
describe('table', () => {
beforeEach(mockLogin);
@@ -231,43 +210,26 @@ describe('FormAttachmentList', () => {
});
});
- // The following tests will be run once as if under IE (where
- // countOfFilesOverDropZone === -1) and once normally (where
- // countOfFilesOverDropZone > 1). The user must be logged in before these
- // tests.
- const testMultipleFiles = (ie) => describe('multiple files', () => {
- describe('drag', () => {
- let app;
- beforeEach(() => {
- testData.extendedFormAttachments.createPast(2);
- // Specifying `route: true` in order to trigger the Vue activated hook,
- // which attaches the jQuery event handlers.
- return loadAttachments({ route: true }).then(component => {
- app = component;
- return trigger.dragenter(
- app,
- '#form-attachment-list-heading div',
- { files: blankFiles(['a', 'b']), ie }
- );
- });
- });
+ /*
+ testMultipleFileSelection() tests the effects of selecting multiple files to
+ upload. It does not test the effects of actually uploading those files: that
+ comes later. However, it tests everything between selecting the files and
+ uploading them.
- it('highlights all the rows of the table', () => {
- for (const tr of app.find('#form-attachment-list-table tbody tr'))
- tr.hasClass('info').should.be.true();
- });
+ The tests will be run under three different scenarios:
- it('shows the popup with the correct text', () => {
- const popup = app.first('#form-attachment-popups-main');
- popup.should.be.visible();
- const text = popup.first('.modal-body').text().trim().iTrim();
- text.should.equal(!ie
- ? 'Drop now to prepare 2 files for upload to this form.'
- : 'Drop now to upload to this form.');
- });
- });
+ 1. The user drops multiple files over the page as if under IE.
+ 2. The user drops multiple files over the page normally (not as if under
+ IE).
+ 3. The user selects multiple files using the file picker dialog.
- describe('drop', () => {
+ For each scenario, the function is passed a callback (`select`) that selects
+ the files.
+
+ The user must be logged in before these tests.
+ */
+ const testMultipleFileSelection = (select) => {
+ describe('table', () => {
let app;
beforeEach(() => {
testData.extendedFormAttachments
@@ -278,11 +240,7 @@ describe('FormAttachmentList', () => {
.then(component => {
app = component;
})
- .then(() => trigger.dragAndDrop(
- app,
- FormAttachmentList,
- { files: blankFiles(['a', 'b', 'd']), ie }
- ));
+ .then(() => select(app, blankFiles(['a', 'b', 'd'])));
});
it('highlights only matching rows', () => {
@@ -299,29 +257,34 @@ describe('FormAttachmentList', () => {
const label = tr[2].find('.label');
if (label.length !== 0) label[0].should.be.hidden();
});
+ });
- it('shows the popup with the correct text', () => {
- const popup = app.first('#form-attachment-popups-main');
- popup.should.be.visible();
- const text = popup.first('.modal-body p').text().trim().iTrim();
- text.should.equal('2 files ready for upload.');
+ describe('after the uploads are canceled', () => {
+ let app;
+ beforeEach(() => {
+ testData.extendedFormAttachments
+ .createPast(1, { name: 'a', exists: true })
+ .createPast(1, { name: 'b', exists: false })
+ .createPast(1, { name: 'c' });
+ return loadAttachments({ route: true })
+ .then(component => {
+ app = component;
+ })
+ .then(() => select(app, blankFiles(['a', 'b', 'd'])))
+ .then(() =>
+ trigger.click(app, '#form-attachment-popups-main .btn-link'));
});
- describe('after the uploads are canceled', () => {
- beforeEach(() =>
- trigger.click(app, '#form-attachment-popups-main .btn-link'));
-
- it('unhighlights the rows', () => {
- app.find('.form-attachment-row-targeted').should.be.empty();
- });
+ it('unhighlights the rows', () => {
+ app.find('.form-attachment-row-targeted').should.be.empty();
+ });
- it('hides the popup', () => {
- app.first('#form-attachment-popups-main').should.be.hidden();
- });
+ it('hides the popup', () => {
+ app.first('#form-attachment-popups-main').should.be.hidden();
});
});
- describe('unmatched files after a drop', () => {
+ describe('unmatched files', () => {
beforeEach(() => {
testData.extendedFormAttachments
.createPast(1, { name: 'a' })
@@ -330,27 +293,20 @@ describe('FormAttachmentList', () => {
});
it('no unmatched files', () =>
- loadAttachments({ route: true })
- .then(app => trigger.dragAndDrop(
- app,
- FormAttachmentList,
- { files: blankFiles(['a', 'b']), ie }
- ))
+ loadAttachments({ route: true, attachToDocument: true })
+ .then(app => select(app, blankFiles(['a', 'b'])))
.then(app => {
const popup = app.first('#form-attachment-popups-main');
popup.should.be.visible();
const text = popup.first('p').text().trim().iTrim();
text.should.equal('2 files ready for upload.');
popup.first('#form-attachment-popups-unmatched').should.be.hidden();
+ popup.first('.btn-primary').should.be.focused();
}));
it('one unmatched file', () =>
- loadAttachments({ route: true })
- .then(app => trigger.dragAndDrop(
- app,
- FormAttachmentList,
- { files: blankFiles(['a', 'd']), ie }
- ))
+ loadAttachments({ route: true, attachToDocument: true })
+ .then(app => select(app, blankFiles(['a', 'd'])))
.then(app => {
const popup = app.first('#form-attachment-popups-main');
popup.should.be.visible();
@@ -361,15 +317,12 @@ describe('FormAttachmentList', () => {
unmatched.first('.icon-exclamation-triangle');
const unmatchedText = unmatched.text().trim().iTrim();
unmatchedText.should.containEql('1 file has a name we don’t recognize and will be ignored.');
+ popup.first('.btn-primary').should.be.focused();
}));
it('multiple unmatched files', () =>
- loadAttachments({ route: true })
- .then(app => trigger.dragAndDrop(
- app,
- FormAttachmentList,
- { files: blankFiles(['a', 'd', 'e']), ie }
- ))
+ loadAttachments({ route: true, attachToDocument: true })
+ .then(app => select(app, blankFiles(['a', 'd', 'e'])))
.then(app => {
const popup = app.first('#form-attachment-popups-main');
popup.should.be.visible();
@@ -380,23 +333,61 @@ describe('FormAttachmentList', () => {
unmatched.first('.icon-exclamation-triangle');
const unmatchedText = unmatched.text().trim().iTrim();
unmatchedText.should.containEql('2 files have a name we don’t recognize and will be ignored.');
+ popup.first('.btn-primary').should.be.focused();
}));
it('all files are unmatched', () =>
loadAttachments({ route: true })
- .then(app => trigger.dragAndDrop(
- app,
- FormAttachmentList,
- { files: blankFiles(['d', 'e']), ie }
- ))
+ .then(app => select(app, blankFiles(['d', 'e'])))
.then(app => {
const popup = app.first('#form-attachment-popups-main');
popup.should.be.visible();
const text = popup.first('.modal-body').text().trim().iTrim();
text.should.containEql('We don’t recognize any of the files you are trying to upload.');
popup.find('#form-attachment-popups-unmatched').should.be.empty();
+ popup.find('.btn-primary').should.be.empty();
}));
});
+ };
+
+ // The following tests will be run once as if under IE (where
+ // countOfFilesOverDropZone === -1) and once normally (where
+ // countOfFilesOverDropZone > 1). The user must be logged in before these
+ // tests.
+ const testMultipleFileDragAndDrop = (ie) => describe('multiple files', () => {
+ describe('drag', () => {
+ let app;
+ beforeEach(() => {
+ testData.extendedFormAttachments.createPast(2);
+ // Specifying `route: true` in order to trigger the Vue activated hook,
+ // which attaches the jQuery event handlers.
+ return loadAttachments({ route: true }).then(component => {
+ app = component;
+ return trigger.dragenter(
+ app,
+ '#form-attachment-list-heading div',
+ { files: blankFiles(['a', 'b']), ie }
+ );
+ });
+ });
+
+ it('highlights all the rows of the table', () => {
+ for (const tr of app.find('#form-attachment-list-table tbody tr'))
+ tr.hasClass('info').should.be.true();
+ });
+
+ it('shows the popup with the correct text', () => {
+ const popup = app.first('#form-attachment-popups-main');
+ popup.should.be.visible();
+ const text = popup.first('.modal-body').text().trim().iTrim();
+ text.should.equal(!ie
+ ? 'Drop now to prepare 2 files for upload to this form.'
+ : 'Drop now to upload to this form.');
+ });
+ });
+
+ describe('drop', () => testMultipleFileSelection((app, files) =>
+ trigger.dragAndDrop(app, FormAttachmentList, { files, ie })));
describe('confirming the uploads', () => {
beforeEach(() => {
@@ -585,6 +576,127 @@ describe('FormAttachmentList', () => {
});
});
+ /*
+ testSingleFileSelection() tests the effects of selecting a single file to
+ upload. It does not test the effects of actually uploading the file: that
+ comes later. However, it tests everything between selecting the file and
+ uploading it.
+
+ The tests will be run under three different scenarios:
+
+ 1. The user drops a single file outside a row of the table as if under IE
+ (where countOfFilesOverDropZone === -1).
+ 2. The user drops a single file outside a row of the table normally
+ (where countOfFilesOverDropZone === 1).
+ 3. The user selects a single file using the file picker dialog.
+
+ The tests are not run under the following scenario, which differs in a few
+ ways:
+
+ - The user drops a single file over an attachment.
+
+ For each scenario, the function is passed a callback (`select`) that selects
+ the file.
+
+ The user must be logged in before these tests.
+ */
+ const testSingleFileSelection = (select) => {
+ const drop = (filename, options = {}) =>
+ loadAttachments({ ...options, route: true })
+ .then(app => select(app, blankFiles([filename])));
+
+ describe('drop', () => {
+ beforeEach(() => {
+ testData.extendedFormAttachments
+ .createPast(1, { name: 'a', exists: true })
+ .createPast(1, { name: 'b', exists: false })
+ .createPast(1, { name: 'c', exists: true })
+ .createPast(1, { name: 'd', exists: false });
+ });
+
+ it('highlights only the matching row', () =>
+ drop('a').then(app => {
+ const targeted = app.find('#form-attachment-list-table tbody tr')
+ .map(tr => tr.hasClass('form-attachment-row-targeted'));
+ targeted.should.eql([true, false, false, false]);
+ }));
+
+ describe('Replace label', () => {
+ it('shows the label when the file matches an existing attachment', () =>
+ drop('a').then(app => {
+ const tr = app.find('#form-attachment-list-table tbody tr');
+ tr[0].first('.label').should.be.visible();
+ tr[1].find('.label').length.should.equal(0);
+ tr[2].first('.label').should.be.hidden();
+ tr[3].find('.label').length.should.equal(0);
+ }));
+
+ it('does not show the label when the file matches a missing attachment', () =>
+ drop('b').then(app => {
+ const tr = app.find('#form-attachment-list-table tbody tr');
+ tr[0].first('.label').should.be.hidden();
+ tr[1].find('.label').length.should.equal(0);
+ tr[2].first('.label').should.be.hidden();
+ tr[3].find('.label').length.should.equal(0);
+ }));
+ });
+
+ it('shows the popup with the correct text', () =>
+ drop('a').then(app => {
+ const popup = app.first('#form-attachment-popups-main');
+ popup.should.be.visible();
+ const text = popup.first('.modal-body p').text().trim().iTrim();
+ text.should.equal('1 file ready for upload.');
+ }));
+
+ describe('after the uploads are canceled', () => {
+ it('unhighlights the rows', () =>
+ drop('a')
+ .then(app =>
+ trigger.click(app, '#form-attachment-popups-main .btn-link'))
+ .then(app => {
+ app.find('.form-attachment-row-targeted').should.be.empty();
+ }));
+
+ it('hides the popup', () =>
+ drop('a')
+ .then(app =>
+ trigger.click(app, '#form-attachment-popups-main .btn-link'))
+ .then(app => {
+ app.first('#form-attachment-popups-main').should.be.hidden();
+ }));
+ });
+ });
+
+ describe('unmatched file after a drop', () => {
+ beforeEach(() => {
+ testData.extendedFormAttachments
+ .createPast(1, { name: 'a' })
+ .createPast(1, { name: 'b' });
+ });
+
+ it('correctly renders when the file matches', () =>
+ drop('a', { attachToDocument: true }).then(app => {
+ const popup = app.first('#form-attachment-popups-main');
+ popup.should.be.visible();
+ const text = popup.first('p').text().trim().iTrim();
+ text.should.equal('1 file ready for upload.');
+ popup.first('#form-attachment-popups-unmatched').should.be.hidden();
+ popup.first('.btn-primary').should.be.focused();
+ }));
+
+ it('correctly renders when the file does not match', () =>
+ drop('c', { attachToDocument: true }).then(app => {
+ const popup = app.first('#form-attachment-popups-main');
+ popup.should.be.visible();
+ const text = popup.first('.modal-body').text().trim().iTrim();
+ text.should.containEql('We don’t recognize the file you are trying to upload.');
+ popup.find('#form-attachment-popups-unmatched').should.be.empty();
+ popup.find('.btn-primary').should.be.empty();
+ }));
+ });
+ };
+
/*
The following tests will be run under four different scenarios:
@@ -765,113 +877,8 @@ describe('FormAttachmentList', () => {
});
});
- describe('drop', () => {
- beforeEach(() => {
- testData.extendedFormAttachments
- .createPast(1, { name: 'a', exists: true })
- .createPast(1, { name: 'b', exists: false })
- .createPast(1, { name: 'c', exists: true })
- .createPast(1, { name: 'd', exists: false });
- });
-
- const drop = (filename) => loadAttachments({ route: true })
- .then(app => trigger.dragAndDrop(
- app,
- FormAttachmentList,
- { files: blankFiles([filename]), ie }
- ));
-
- it('highlights only the matching row', () =>
- drop('a').then(app => {
- const targeted = app.find('#form-attachment-list-table tbody tr')
- .map(tr => tr.hasClass('form-attachment-row-targeted'));
- targeted.should.eql([true, false, false, false]);
- }));
-
- describe('Replace label', () => {
- it('shows the label when the file matches an existing attachment', () =>
- drop('a').then(app => {
- const tr = app.find('#form-attachment-list-table tbody tr');
- tr[0].first('.label').should.be.visible();
- tr[1].find('.label').length.should.equal(0);
- tr[2].first('.label').should.be.hidden();
- tr[3].find('.label').length.should.equal(0);
- }));
-
- it('does not show the label when the file matches a missing attachment', () =>
- drop('b').then(app => {
- const tr = app.find('#form-attachment-list-table tbody tr');
- tr[0].first('.label').should.be.hidden();
- tr[1].find('.label').length.should.equal(0);
- tr[2].first('.label').should.be.hidden();
- tr[3].find('.label').length.should.equal(0);
- }));
- });
-
- it('shows the popup with the correct text', () =>
- drop('a').then(app => {
- const popup = app.first('#form-attachment-popups-main');
- popup.should.be.visible();
- const text = popup.first('.modal-body p').text().trim().iTrim();
- text.should.equal('1 file ready for upload.');
- }));
-
- describe('after the uploads are canceled', () => {
- it('unhighlights the rows', () =>
- drop('a')
- .then(app =>
- trigger.click(app, '#form-attachment-popups-main .btn-link'))
- .then(app => {
- app.find('.form-attachment-row-targeted').should.be.empty();
- }));
-
- it('hides the popup', () =>
- drop('a')
- .then(app =>
- trigger.click(app, '#form-attachment-popups-main .btn-link'))
- .then(app => {
- app.first('#form-attachment-popups-main').should.be.hidden();
- }));
- });
- });
-
- describe('unmatched file after a drop', () => {
- beforeEach(() => {
- testData.extendedFormAttachments
- .createPast(1, { name: 'a' })
- .createPast(1, { name: 'b' });
- });
-
- it('correctly renders when the file matches', () =>
- loadAttachments({ route: true })
- .then(app => trigger.dragAndDrop(
- app,
- FormAttachmentList,
- { files: blankFiles(['a']), ie }
- ))
- .then(app => {
- const popup = app.first('#form-attachment-popups-main');
- popup.should.be.visible();
- const text = popup.first('p').text().trim().iTrim();
- text.should.equal('1 file ready for upload.');
- popup.first('#form-attachment-popups-unmatched').should.be.hidden();
- }));
-
- it('correctly renders when the file does not match', () =>
- loadAttachments({ route: true })
- .then(app => trigger.dragAndDrop(
- app,
- FormAttachmentList,
- { files: blankFiles(['c']), ie }
- ))
- .then(app => {
- const popup = app.first('#form-attachment-popups-main');
- popup.should.be.visible();
- const text = popup.first('.modal-body').text().trim().iTrim();
- text.should.containEql('We don’t recognize the file you are trying to upload.');
- popup.find('#form-attachment-popups-unmatched').should.be.empty();
- }));
- });
+ testSingleFileSelection((app, files) =>
+ trigger.dragAndDrop(app, FormAttachmentList, { files, ie }));
describe('confirming the upload', () =>
testSingleFileUpload(attachmentName => loadAttachments({ route: true })
@@ -888,163 +895,203 @@ describe('FormAttachmentList', () => {
describe(ie ? 'using IE' : 'not using IE', () => {
beforeEach(mockLogin);
- testMultipleFiles(ie);
+ testMultipleFileDragAndDrop(ie);
testSingleFileOutsideRow(ie);
});
}
- describe('drag a single file over a row of the table', () => {
+ describe('upload files modal', () => {
beforeEach(mockLogin);
- it('highlights only the target row', () => {
- testData.extendedFormAttachments.createPast(2);
- return loadAttachments({ route: true })
- .then(app => trigger.dragenter(
- app,
- '#form-attachment-list-table tbody tr',
- blankFiles(['a'])
- ))
- .then(app => {
- const tr = app.find('#form-attachment-list-table tbody tr');
- tr[0].hasClass('info').should.be.true();
- tr[0].hasClass('form-attachment-row-targeted').should.be.true();
- tr[1].hasClass('info').should.be.false();
- });
- });
+ describe('state', () => {
+ beforeEach(() => {
+ testData.extendedFormAttachments
+ .createPast(1, { name: 'a' })
+ .createPast(1, { name: 'b' });
+ });
- it('shows a Replace label if the attachment exists', () => {
- testData.extendedFormAttachments.createPast(2, { exists: true });
- return loadAttachments({ route: true })
- .then(app => trigger.dragenter(
- app,
- '#form-attachment-list-table tbody tr',
- blankFiles(['a'])
- ))
- .then(app => {
- const labels = app.find('#form-attachment-list-table .label');
- labels.length.should.equal(2);
- labels[0].should.be.visible();
- labels[1].should.be.hidden();
- });
- });
+ it('is initially hidden', () =>
+ loadAttachments().then(component => {
+ const modal = component.first(FormAttachmentUploadFiles);
+ modal.getProp('state').should.be.false();
+ }));
- it('does not show a Replace label if the attachment does not exist', () => {
- testData.extendedFormAttachments.createPast(2, { exists: false });
- return loadAttachments({ route: true })
- .then(app => trigger.dragenter(
- app,
- '#form-attachment-list-table tbody tr',
- blankFiles(['a'])
- ))
- .then(app => {
- app.find('#form-attachment-list-table .label').length.should.equal(0);
- });
+ it('is shown after button click', () =>
+ loadAttachments()
+ .then(component =>
+ trigger.click(component, '#form-attachment-list-heading button'))
+ .then(component => {
+ const modal = component.first(FormAttachmentUploadFiles);
+ modal.getProp('state').should.be.true();
+ }));
});
- it('shows the popup with the correct text', () => {
- testData.extendedFormAttachments
- .createPast(1, { name: 'first_attachment' })
- .createPast(1, { name: 'second_attachment' });
- return loadAttachments({ route: true })
- .then(app => trigger.dragenter(
- app,
- '#form-attachment-list-table tbody tr',
- blankFiles(['a'])
- ))
- .then(app => {
- const popup = app.first('#form-attachment-popups-main');
- popup.should.be.visible();
- const text = popup.first('.modal-body').text().trim().iTrim();
- text.should.equal('Drop now to upload this file as first_attachment.');
- });
- });
+ const select = (app, files) =>
+ trigger.click(app, '#form-attachment-list-heading button')
+ .then(() =>
+ trigger.click(app, '#form-attachment-upload-files a[role="button"]'))
+ .then(() => {
+ const input = app.first('#form-attachment-upload-files input[type="file"]');
+ const target = { files: dataTransfer(files).files };
+ const event = $.Event('change', { target });
+ $(input.element).trigger(event);
+ return app.vm.$nextTick();
+ })
+ .then(() => app);
+ describe('select single file', () => testSingleFileSelection(select));
+ describe('select multiple files', () => testMultipleFileSelection(select));
});
- describe('drop a single file over an attachment with the same name', () => {
+ describe('dragging and dropping a single file over a row', () => {
beforeEach(mockLogin);
- testSingleFileUpload(attachmentName => loadAttachments({ route: true })
- .complete()
- .request(app => {
- const tr = app.find('#form-attachment-list-table tbody tr');
- tr.length.should.equal(testData.extendedFormAttachments.size);
- for (let i = 0; i < testData.extendedFormAttachments.size; i += 1) {
- if (testData.extendedFormAttachments.get(i).name === attachmentName)
- return trigger.dragAndDrop(tr[i], blankFiles([attachmentName]))
- .then(() => app);
- }
- throw new Error('matching attachment not found');
- }));
- });
+ describe('drag over a row of the table', () => {
+ it('highlights only the target row', () => {
+ testData.extendedFormAttachments.createPast(2);
+ return loadAttachments({ route: true })
+ .then(app => trigger.dragenter(
+ app,
+ '#form-attachment-list-table tbody tr',
+ blankFiles(['a'])
+ ))
+ .then(app => {
+ const tr = app.find('#form-attachment-list-table tbody tr');
+ tr[0].hasClass('info').should.be.true();
+ tr[0].hasClass('form-attachment-row-targeted').should.be.true();
+ tr[1].hasClass('info').should.be.false();
+ });
+ });
- describe('drop a single file over an attachment with a different name', () => {
- beforeEach(mockLogin);
+ it('shows a Replace label if the attachment exists', () => {
+ testData.extendedFormAttachments.createPast(2, { exists: true });
+ return loadAttachments({ route: true })
+ .then(app => trigger.dragenter(
+ app,
+ '#form-attachment-list-table tbody tr',
+ blankFiles(['a'])
+ ))
+ .then(app => {
+ const labels = app.find('#form-attachment-list-table .label');
+ labels.length.should.equal(2);
+ labels[0].should.be.visible();
+ labels[1].should.be.hidden();
+ });
+ });
- const dropMismatchingFile = (attachmentName) =>
- loadAttachments({ route: true }).afterResponses(app => {
- const tr = app.find('#form-attachment-list-table tbody tr');
- const attachments = testData.extendedFormAttachments.sorted();
- tr.length.should.equal(attachments.length);
- for (let i = 0; i < tr.length; i += 1) {
- if (attachments[i].name === attachmentName)
- return trigger.dragAndDrop(tr[i], blankFiles(['mismatching_file']))
- .then(() => app);
- }
- throw new Error('matching attachment not found');
+ it('does not show a Replace label if the attachment does not exist', () => {
+ testData.extendedFormAttachments.createPast(2, { exists: false });
+ return loadAttachments({ route: true })
+ .then(app => trigger.dragenter(
+ app,
+ '#form-attachment-list-table tbody tr',
+ blankFiles(['a'])
+ ))
+ .then(app => {
+ app.find('#form-attachment-list-table .label').length.should.equal(0);
+ });
});
- describe('name mismatch modal', () => {
- beforeEach(() => {
+ it('shows the popup with the correct text', () => {
testData.extendedFormAttachments
- .createPast(1, { name: 'a', exists: true })
- .createPast(1, { name: 'b', exists: false });
+ .createPast(1, { name: 'first_attachment' })
+ .createPast(1, { name: 'second_attachment' });
+ return loadAttachments({ route: true })
+ .then(app => trigger.dragenter(
+ app,
+ '#form-attachment-list-table tbody tr',
+ blankFiles(['a'])
+ ))
+ .then(app => {
+ const popup = app.first('#form-attachment-popups-main');
+ popup.should.be.visible();
+ const text = popup.first('.modal-body').text().trim().iTrim();
+ text.should.equal('Drop now to upload this file as first_attachment.');
+ });
});
+ });
- it('is initially hidden', () =>
- loadAttachments({ route: true })
- .then(app => {
- const modal = app.first(FormAttachmentNameMismatch);
- modal.getProp('state').should.be.false();
- }));
+ describe('drop over an attachment with the same name', () => {
+ testSingleFileUpload(attachmentName => loadAttachments({ route: true })
+ .complete()
+ .request(app => {
+ const tr = app.find('#form-attachment-list-table tbody tr');
+ tr.length.should.equal(testData.extendedFormAttachments.size);
+ for (let i = 0; i < testData.extendedFormAttachments.size; i += 1) {
+ if (testData.extendedFormAttachments.get(i).name === attachmentName)
+ return trigger.dragAndDrop(tr[i], blankFiles([attachmentName]))
+ .then(() => app);
+ }
+ throw new Error('matching attachment not found');
+ }));
+ });
- it('is shown after the drop', () =>
- dropMismatchingFile('a')
- .then(app => {
- const modal = app.first(FormAttachmentNameMismatch);
- modal.getProp('state').should.be.true();
- }));
+ describe('drop over an attachment with a different name', () => {
+ const dropMismatchingFile = (attachmentName) =>
+ loadAttachments({ route: true }).afterResponses(app => {
+ const tr = app.find('#form-attachment-list-table tbody tr');
+ const attachments = testData.extendedFormAttachments.sorted();
+ tr.length.should.equal(attachments.length);
+ for (let i = 0; i < tr.length; i += 1) {
+ if (attachments[i].name === attachmentName)
+ return trigger.dragAndDrop(tr[i], blankFiles(['mismatching_file']))
+ .then(() => app);
+ }
+ throw new Error('matching attachment not found');
+ });
- it('is hidden upon cancel', () =>
- dropMismatchingFile('a')
- .then(app => {
- const modal = app.first(FormAttachmentNameMismatch);
- return trigger.click(modal, '.btn-link');
- })
- .then(modal => {
- modal.getProp('state').should.be.false();
- }));
+ describe('name mismatch modal', () => {
+ beforeEach(() => {
+ testData.extendedFormAttachments
+ .createPast(1, { name: 'a', exists: true })
+ .createPast(1, { name: 'b', exists: false });
+ });
- it('renders correctly for an existing attachment', () =>
- dropMismatchingFile('a')
- .then(app => {
- const modal = app.first(FormAttachmentNameMismatch);
- const title = modal.first('.modal-title').text().trim();
- title.should.equal('Replace File');
- }));
+ it('is initially hidden', () =>
+ loadAttachments({ route: true })
+ .then(app => {
+ const modal = app.first(FormAttachmentNameMismatch);
+ modal.getProp('state').should.be.false();
+ }));
- it('renders correctly for a missing attachment', () =>
- dropMismatchingFile('b')
- .then(app => {
- const modal = app.first(FormAttachmentNameMismatch);
- const title = modal.first('.modal-title').text().trim();
- title.should.equal('Upload File');
- }));
- });
+ it('is shown after the drop', () =>
+ dropMismatchingFile('a')
+ .then(app => {
+ const modal = app.first(FormAttachmentNameMismatch);
+ modal.getProp('state').should.be.true();
+ }));
+
+ it('is hidden upon cancel', () =>
+ dropMismatchingFile('a')
+ .then(app => {
+ const modal = app.first(FormAttachmentNameMismatch);
+ return trigger.click(modal, '.btn-link');
+ })
+ .then(modal => {
+ modal.getProp('state').should.be.false();
+ }));
+
+ it('renders correctly for an existing attachment', () =>
+ dropMismatchingFile('a')
+ .then(app => {
+ const modal = app.first(FormAttachmentNameMismatch);
+ const title = modal.first('.modal-title').text().trim();
+ title.should.equal('Replace File');
+ }));
- testSingleFileUpload(attachmentName => dropMismatchingFile(attachmentName)
- .request(app => {
- const modal = app.first(FormAttachmentNameMismatch);
- return trigger.click(modal, '.btn-primary').then(() => app);
- }));
+ it('renders correctly for a missing attachment', () =>
+ dropMismatchingFile('b')
+ .then(app => {
+ const modal = app.first(FormAttachmentNameMismatch);
+ const title = modal.first('.modal-title').text().trim();
+ title.should.equal('Upload File');
+ }));
+ });
+
+ testSingleFileUpload(attachmentName => dropMismatchingFile(attachmentName)
+ .request(app => {
+ const modal = app.first(FormAttachmentNameMismatch);
+ return trigger.click(modal, '.btn-primary').then(() => app);
+ }));
+ });
});
});