From a1083ca253ba1db84481b8d3b05f5d068bcb9fd3 Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Wed, 8 Jan 2020 18:15:03 +0100 Subject: [PATCH 1/2] javascript: add some comments to the upload systems --- app/javascript/shared/activestorage/ujs.js | 2 ++ app/javascript/shared/activestorage/uploader.js | 2 +- app/javascript/shared/remote-poller.js | 7 +++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/javascript/shared/activestorage/ujs.js b/app/javascript/shared/activestorage/ujs.js index 8efd98b86..3f8109953 100644 --- a/app/javascript/shared/activestorage/ujs.js +++ b/app/javascript/shared/activestorage/ujs.js @@ -27,6 +27,8 @@ addUploadEventListener(INITIALIZE_EVENT, ({ target, detail: { id, file } }) => { addUploadEventListener(START_EVENT, ({ target, detail: { id } }) => { ProgressBar.start(id); + // At the end of the upload, the form will be submitted again. + // Avoid the confirm dialog to be presented again then. const button = target.form.querySelector('button.primary'); if (button) { button.removeAttribute('data-confirm'); diff --git a/app/javascript/shared/activestorage/uploader.js b/app/javascript/shared/activestorage/uploader.js index 20363e4a8..63cab306f 100644 --- a/app/javascript/shared/activestorage/uploader.js +++ b/app/javascript/shared/activestorage/uploader.js @@ -3,7 +3,7 @@ import ProgressBar from './progress-bar'; /** Uploader class is a delegate for DirectUpload instance - used to track lifecycle and progress of un upload. + used to track lifecycle and progress of an upload. */ export default class Uploader { constructor(input, file, directUploadUrl) { diff --git a/app/javascript/shared/remote-poller.js b/app/javascript/shared/remote-poller.js index 6aca55745..937810778 100644 --- a/app/javascript/shared/remote-poller.js +++ b/app/javascript/shared/remote-poller.js @@ -29,6 +29,13 @@ delegate('click', '[data-attachment-refresh]', event => { attachementPoller.check(); }); +// Periodically check the state of a set of URLs. +// +// Each time the given URL is requested, the matching `show.js.erb` view is rendered, +// causing the state to be refreshed. +// +// This is used mainly to refresh attachments during the anti-virus check, +// but also to refresh the state of a pending spreadsheet export. class RemotePoller { urls = new Set(); timeout; From 6417c0d2c02bed07ba0951bb84c2d9032492c04f Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Mon, 30 Mar 2020 13:34:56 +0000 Subject: [PATCH 2/2] dossiers: allow auto upload of attachments --- app/assets/images/icons/retry.svg | 1 + .../stylesheets/new_design/_colors.scss | 1 + .../stylesheets/new_design/attachment.scss | 33 ++++ .../new_design/direct_uploads.scss | 3 +- app/assets/stylesheets/new_design/icons.scss | 4 + .../champs/piece_justificative_controller.rb | 21 +++ app/helpers/champ_helper.rb | 6 + .../dossiers/auto-upload-controller.js | 141 ++++++++++++++++++ .../new_design/dossiers/auto-upload.js | 23 +++ .../dossiers/auto-uploads-controllers.js | 46 ++++++ app/javascript/packs/application.js | 1 + .../shared/activestorage/progress-bar.js | 2 +- app/javascript/shared/utils.js | 10 +- .../champs/piece_justificative/show.js.erb | 17 +++ app/views/root/patron.html.haml | 1 + app/views/shared/attachment/_edit.html.haml | 12 +- .../editable_champs/_editable_champ.html.haml | 2 +- config/initializers/flipper.rb | 1 + config/routes.rb | 1 + .../piece_justificative_controller_spec.rb | 65 ++++++++ spec/features/users/brouillon_spec.rb | 90 ++++++++++- spec/fixtures/files/invalid_file_format.json | 3 + 22 files changed, 475 insertions(+), 9 deletions(-) create mode 100644 app/assets/images/icons/retry.svg create mode 100644 app/controllers/champs/piece_justificative_controller.rb create mode 100644 app/javascript/new_design/dossiers/auto-upload-controller.js create mode 100644 app/javascript/new_design/dossiers/auto-upload.js create mode 100644 app/javascript/new_design/dossiers/auto-uploads-controllers.js create mode 100644 app/views/champs/piece_justificative/show.js.erb create mode 100644 spec/controllers/champs/piece_justificative_controller_spec.rb create mode 100644 spec/fixtures/files/invalid_file_format.json diff --git a/app/assets/images/icons/retry.svg b/app/assets/images/icons/retry.svg new file mode 100644 index 000000000..84797f123 --- /dev/null +++ b/app/assets/images/icons/retry.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/stylesheets/new_design/_colors.scss b/app/assets/stylesheets/new_design/_colors.scss index f1bd66076..052e2a51b 100644 --- a/app/assets/stylesheets/new_design/_colors.scss +++ b/app/assets/stylesheets/new_design/_colors.scss @@ -10,6 +10,7 @@ $dark-red: #A10005; $medium-red: rgba(161, 0, 5, 0.9); $light-red: #ED1C24; $lighter-red: #F52A2A; +$background-red: #FFDFDF; $green: #15AD70; $lighter-green: lighten($green, 30%); $light-green: lighten($green, 25%); diff --git a/app/assets/stylesheets/new_design/attachment.scss b/app/assets/stylesheets/new_design/attachment.scss index 27c585d63..20d21a2fc 100644 --- a/app/assets/stylesheets/new_design/attachment.scss +++ b/app/assets/stylesheets/new_design/attachment.scss @@ -1,3 +1,4 @@ +@import "colors"; @import "constants"; .attachment-actions { @@ -13,6 +14,38 @@ } } +.attachment-error { + display: flex; + width: max-content; + max-width: 100%; + align-items: center; + margin-bottom: $default-padding; + padding: $default-padding; + background: $background-red; + + &.hidden { + display: none; + } +} + +.attachment-error-message { + display: inline-block; + margin-right: $default-padding; + color: $medium-red; +} + +.attachment-error-title { + font-weight: bold; +} + +.attachment-error-retry { + white-space: nowrap; + + &.hidden { + display: none; + } +} + .attachment-input.hidden { display: none; } diff --git a/app/assets/stylesheets/new_design/direct_uploads.scss b/app/assets/stylesheets/new_design/direct_uploads.scss index febde1bd0..54fa203b1 100644 --- a/app/assets/stylesheets/new_design/direct_uploads.scss +++ b/app/assets/stylesheets/new_design/direct_uploads.scss @@ -33,6 +33,7 @@ border-color: red; } -input[type=file][data-direct-upload-url][disabled] { +input[type=file][data-direct-upload-url][disabled], +input[type=file][data-auto-attach-url][disabled] { display: none; } diff --git a/app/assets/stylesheets/new_design/icons.scss b/app/assets/stylesheets/new_design/icons.scss index c95fc87df..a05037802 100644 --- a/app/assets/stylesheets/new_design/icons.scss +++ b/app/assets/stylesheets/new_design/icons.scss @@ -67,6 +67,10 @@ background-image: image-url("icons/preview.svg"); } + &.retry { + background-image: image-url("icons/retry.svg"); + } + &.download { background-image: image-url("icons/download.svg"); } diff --git a/app/controllers/champs/piece_justificative_controller.rb b/app/controllers/champs/piece_justificative_controller.rb new file mode 100644 index 000000000..3aa6578b7 --- /dev/null +++ b/app/controllers/champs/piece_justificative_controller.rb @@ -0,0 +1,21 @@ +class Champs::PieceJustificativeController < ApplicationController + before_action :authenticate_logged_user! + + def update + @champ = policy_scope(Champ).find(params[:champ_id]) + + @champ.piece_justificative_file.attach(params[:blob_signed_id]) + if @champ.save + render :show + else + errors = @champ.errors.full_messages + + # Before Rails 6, the attachment was persisted to database + # by 'attach', even before calling save. + # So until we're on Rails 6, we need to purge the file explicitely. + @champ.piece_justificative_file.purge_later + + render :json => { errors: errors }, :status => 422 + end + end +end diff --git a/app/helpers/champ_helper.rb b/app/helpers/champ_helper.rb index a31ca1b58..ca6aa40b6 100644 --- a/app/helpers/champ_helper.rb +++ b/app/helpers/champ_helper.rb @@ -31,4 +31,10 @@ module ChampHelper "desc-#{champ.type_de_champ.id}-#{champ.row}" end end + + def auto_attach_url(form, object) + if feature_enabled?(:autoupload_dossier_attachments) && object.is_a?(Champ) && object.public? + champs_piece_justificative_url(form.index) + end + end end diff --git a/app/javascript/new_design/dossiers/auto-upload-controller.js b/app/javascript/new_design/dossiers/auto-upload-controller.js new file mode 100644 index 000000000..1a5b91282 --- /dev/null +++ b/app/javascript/new_design/dossiers/auto-upload-controller.js @@ -0,0 +1,141 @@ +import Uploader from '../../shared/activestorage/uploader'; +import ProgressBar from '../../shared/activestorage/progress-bar'; +import { ajax, show, hide, toggle } from '@utils'; + +// Given a file input in a champ with a selected file, upload a file, +// then attach it to the dossier. +// +// On success, the champ is replaced by an HTML fragment describing the attachment. +// On error, a error message is displayed above the input. +export default class AutoUploadController { + constructor(input, file) { + this.input = input; + this.file = file; + } + + async start() { + try { + this._begin(); + + // Sanity checks + const autoAttachUrl = this.input.dataset.autoAttachUrl; + if (!autoAttachUrl) { + throw new Error('L’attribut "data-auto-attach-url" est manquant'); + } + + const champ = this.input.closest('.editable-champ[data-champ-id]'); + if (!champ) { + throw new Error('Impossible de trouver l’élément ".editable-champ"'); + } + const champId = champ.dataset.champId; + + // Upload the file (using Direct Upload) + let blobSignedId = await this._upload(); + + // Attach the blob to the champ + // (The request responds with Javascript, which displays the attachment HTML fragment). + await this._attach(champId, blobSignedId, autoAttachUrl); + + // Everything good: clear the original file input value + this.input.value = null; + } catch (error) { + this._failed(error); + throw error; + } finally { + this._done(); + } + } + + _begin() { + this.input.disabled = true; + this._hideErrorMessage(); + } + + async _upload() { + const uploader = new Uploader( + this.input, + this.file, + this.input.dataset.directUploadUrl + ); + return await uploader.start(); + } + + async _attach(champId, blobSignedId, autoAttachUrl) { + // Now that the upload is done, display a new progress bar + // to show that the attachment request is still pending. + const progressBar = new ProgressBar(this.input, champId, this.file); + progressBar.progress(100); + progressBar.end(); + + const attachmentRequest = { + url: autoAttachUrl, + type: 'PUT', + data: `champ_id=${champId}&blob_signed_id=${blobSignedId}` + }; + await ajax(attachmentRequest); + + // The progress bar has been destroyed by the attachment HTML fragment that replaced the input, + // so no further cleanup is needed. + } + + _failed(error) { + if (!document.body.contains(this.input)) { + return; + } + + let progressBar = this.input.parentElement.querySelector('.direct-upload'); + if (progressBar) { + progressBar.remove(); + } + + this._displayErrorMessage(error); + } + + _done() { + this.input.disabled = false; + } + + _messageFromError(error) { + if ( + error.xhr && + error.xhr.status == 422 && + error.response && + error.response.errors && + error.response.errors[0] + ) { + return { + title: error.response.errors[0], + description: '', + retry: false + }; + } else { + return { + title: 'Une erreur s’est produite pendant l’envoi du fichier.', + description: error.message || error.toString(), + retry: true + }; + } + } + + _displayErrorMessage(error) { + let errorNode = this.input.parentElement.querySelector('.attachment-error'); + if (errorNode) { + show(errorNode); + let message = this._messageFromError(error); + errorNode.querySelector('.attachment-error-title').textContent = + message.title || ''; + errorNode.querySelector('.attachment-error-description').textContent = + message.description || ''; + toggle(errorNode.querySelector('.attachment-error-retry'), message.retry); + } + } + + _hideErrorMessage() { + let errorElement = this.input.parentElement.querySelector( + '.attachment-error' + ); + if (errorElement) { + hide(errorElement); + } + } +} diff --git a/app/javascript/new_design/dossiers/auto-upload.js b/app/javascript/new_design/dossiers/auto-upload.js new file mode 100644 index 000000000..627a61000 --- /dev/null +++ b/app/javascript/new_design/dossiers/auto-upload.js @@ -0,0 +1,23 @@ +import AutoUploadsControllers from './auto-uploads-controllers.js'; +import { delegate } from '@utils'; + +// Create a controller responsible for managing several concurrent uploads. +const autoUploadsControllers = new AutoUploadsControllers(); + +function startUpload(input) { + Array.from(input.files).forEach(file => { + autoUploadsControllers.upload(input, file); + }); +} + +const fileInputSelector = `input[type=file][data-direct-upload-url][data-auto-attach-url]:not([disabled])`; +delegate('change', fileInputSelector, event => { + startUpload(event.target); +}); + +const retryButtonSelector = `button.attachment-error-retry`; +delegate('click', retryButtonSelector, event => { + const inputSelector = event.target.dataset.inputTarget; + const input = document.querySelector(inputSelector); + startUpload(input); +}); diff --git a/app/javascript/new_design/dossiers/auto-uploads-controllers.js b/app/javascript/new_design/dossiers/auto-uploads-controllers.js new file mode 100644 index 000000000..a50b083ac --- /dev/null +++ b/app/javascript/new_design/dossiers/auto-uploads-controllers.js @@ -0,0 +1,46 @@ +import Rails from '@rails/ujs'; +import AutoUploadController from './auto-upload-controller.js'; + +// Manage multiple concurrent uploads. +// +// When the first upload starts, all the form "Submit" buttons are disabled. +// They are enabled again when the last upload ends. +export default class AutoUploadsControllers { + constructor() { + this.inFlightUploadsCount = 0; + } + + async upload(input, file) { + let form = input.form; + this._incrementInFlightUploads(form); + + try { + let controller = new AutoUploadController(input, file); + await controller.start(); + } finally { + this._decrementInFlightUploads(form); + } + } + + _incrementInFlightUploads(form) { + this.inFlightUploadsCount += 1; + + if (form) { + form + .querySelectorAll('button[type=submit]') + .forEach(submitButton => Rails.disableElement(submitButton)); + } + } + + _decrementInFlightUploads(form) { + if (this.inFlightUploadsCount > 0) { + this.inFlightUploadsCount -= 1; + } + + if (this.inFlightUploadsCount == 0 && form) { + form + .querySelectorAll('button[type=submit]') + .forEach(submitButton => Rails.enableElement(submitButton)); + } + } +} diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 5d33fb7ae..b436909dd 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -23,6 +23,7 @@ import '../new_design/select2'; import '../new_design/spinner'; import '../new_design/support'; import '../new_design/dossiers/auto-save'; +import '../new_design/dossiers/auto-upload'; import '../new_design/champs/carte'; import '../new_design/champs/linked-drop-down-list'; diff --git a/app/javascript/shared/activestorage/progress-bar.js b/app/javascript/shared/activestorage/progress-bar.js index e08df104d..14efed061 100644 --- a/app/javascript/shared/activestorage/progress-bar.js +++ b/app/javascript/shared/activestorage/progress-bar.js @@ -41,7 +41,7 @@ export default class ProgressBar { } static render(id, filename) { - return `
+ return `
${filename}
`; diff --git a/app/javascript/shared/utils.js b/app/javascript/shared/utils.js index 26603d954..d360af45f 100644 --- a/app/javascript/shared/utils.js +++ b/app/javascript/shared/utils.js @@ -13,8 +13,14 @@ export function hide(el) { el && el.classList.add('hidden'); } -export function toggle(el) { - el && el.classList.toggle('hidden'); +export function toggle(el, force) { + if (force == undefined) { + el & el.classList.toggle('hidden'); + } else if (force) { + el && el.classList.remove('hidden'); + } else { + el && el.classList.add('hidden'); + } } export function enable(el) { diff --git a/app/views/champs/piece_justificative/show.js.erb b/app/views/champs/piece_justificative/show.js.erb new file mode 100644 index 000000000..e921f9556 --- /dev/null +++ b/app/views/champs/piece_justificative/show.js.erb @@ -0,0 +1,17 @@ +<% dossier = @champ.dossier %> + +<%= fields_for dossier do |form| %> + <%= form.fields_for :champs, dossier.champs.where(id: @champ.id), include_id: false do |champ_form| %> + <% render_to_element(".editable-champ[data-champ-id=\"#{@champ.id}\"]", + partial: 'shared/dossiers/editable_champs/editable_champ', + locals: { + champ: @champ, + form: champ_form + }) %> + <% end %> +<% end %> + +<% attachment = @champ.piece_justificative_file.attachment %> +<% if attachment.virus_scanner.pending? %> + <%= fire_event('attachment:update', { url: attachment_url(attachment.id, { signed_id: attachment.blob.signed_id, user_can_upload: true }) }.to_json ) %> +<% end %> diff --git a/app/views/root/patron.html.haml b/app/views/root/patron.html.haml index 507cb1ee1..0a66b983d 100644 --- a/app/views/root/patron.html.haml +++ b/app/views/root/patron.html.haml @@ -32,6 +32,7 @@ %span.icon.phone %span.icon.clock %span.icon.preview + %span.icon.retry %span.icon.download %span.icon.download-white %span.icon.move-handle diff --git a/app/views/shared/attachment/_edit.html.haml b/app/views/shared/attachment/_edit.html.haml index e250fd6e8..9c4cfbd35 100644 --- a/app/views/shared/attachment/_edit.html.haml +++ b/app/views/shared/attachment/_edit.html.haml @@ -22,7 +22,17 @@ .attachment-action = button_tag 'Remplacer', type: 'button', class: 'button small', data: { 'toggle-target': ".attachment-input-#{attachment_id}" } + .attachment-error.hidden + .attachment-error-message + %p.attachment-error-title + Une erreur s’est produite pendant l’envoi du fichier. + %p.attachment-error-description + = button_tag type: 'button', class: 'button attachment-error-retry', data: { 'input-target': ".attachment-input-#{attachment_id}" } do + %span.icon.retry + Ré-essayer + = form.file_field attached_file.name, class: "attachment-input attachment-input-#{attachment_id} #{'hidden' if persisted}", accept: accept, - direct_upload: true + direct_upload: true, + data: { 'auto-attach-url': auto_attach_url(form, form.object) } diff --git a/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml b/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml index 1d7b79355..a446bf5f7 100644 --- a/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml +++ b/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml @@ -1,4 +1,4 @@ -.editable-champ{ class: "editable-champ-#{champ.type_champ}" } +.editable-champ{ class: "editable-champ-#{champ.type_champ}", data: { 'champ-id': champ.id } } - if champ.repetition? %h3.header-subsection= champ.libelle - if champ.description.present? diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb index d434f6a72..e19f6e978 100644 --- a/config/initializers/flipper.rb +++ b/config/initializers/flipper.rb @@ -31,6 +31,7 @@ features = [ :insee_api_v3, :instructeur_bypass_email_login_token, :autosave_dossier_draft, + :autoupload_dossier_attachments, :maintenance_mode, :mini_profiler, :operation_log_serialize_subject, diff --git a/config/routes.rb b/config/routes.rb index c3cb95419..112a71b5b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -119,6 +119,7 @@ Rails.application.routes.draw do get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link post ':position/carte', to: 'carte#show', as: :carte post ':position/repetition', to: 'repetition#show', as: :repetition + put ':position/piece_justificative', to: 'piece_justificative#update', as: :piece_justificative end get 'attachments/:id', to: 'attachments#show', as: :attachment diff --git a/spec/controllers/champs/piece_justificative_controller_spec.rb b/spec/controllers/champs/piece_justificative_controller_spec.rb new file mode 100644 index 000000000..493d4d46f --- /dev/null +++ b/spec/controllers/champs/piece_justificative_controller_spec.rb @@ -0,0 +1,65 @@ +describe Champs::PieceJustificativeController, type: :controller do + let(:user) { create(:user) } + let(:procedure) { create(:procedure, :published, :with_piece_justificative) } + let(:dossier) { create(:dossier, user: user, procedure: procedure) } + let(:champ) { dossier.champs.first } + + describe '#update' do + render_views + before { sign_in user } + + subject do + put :update, params: { + position: '1', + champ_id: champ.id, + blob_signed_id: file + }, format: 'js' + end + + context 'when the file is valid' do + let(:file) { Rack::Test::UploadedFile.new('spec/fixtures/files/piece_justificative_0.pdf', 'application/pdf') } + + it 'attach the file' do + subject + champ.reload + expect(champ.piece_justificative_file.attached?).to be true + expect(champ.piece_justificative_file.filename).to eq('piece_justificative_0.pdf') + end + + it 'renders the attachment template as Javascript' do + subject + expect(response.status).to eq(200) + expect(response.body).to include("editable-champ[data-champ-id=\"#{champ.id}\"]") + end + end + + context 'when the file is invalid' do + let(:file) { Rack::Test::UploadedFile.new('spec/fixtures/files/invalid_file_format.json', 'application/json') } + + # TODO: for now there are no validators on the champ piece_justificative_file, + # so we have to mock a failing validation. + # Once the validators will be enabled, remove those mocks, and let the usual + # validation fail naturally. + # + # See https://github.com/betagouv/demarches-simplifiees.fr/issues/4926 + before do + champ + expect_any_instance_of(Champs::PieceJustificativeChamp).to receive(:save).and_return(false) + expect_any_instance_of(Champs::PieceJustificativeChamp).to receive(:errors) + .and_return(double(full_messages: ['La pièce justificative n’est pas d’un type accepté'])) + end + + it 'doesn’t attach the file' do + subject + expect(champ.reload.piece_justificative_file.attached?).to be false + end + + it 'renders an error' do + subject + expect(response.status).to eq(422) + expect(response.header['Content-Type']).to include('application/json') + expect(JSON.parse(response.body)).to eq({ 'errors' => ['La pièce justificative n’est pas d’un type accepté'] }) + end + end + end +end diff --git a/spec/features/users/brouillon_spec.rb b/spec/features/users/brouillon_spec.rb index 2b45260b4..cd992974f 100644 --- a/spec/features/users/brouillon_spec.rb +++ b/spec/features/users/brouillon_spec.rb @@ -5,9 +5,6 @@ feature 'The user' do let!(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs_mandatory) } let(:user_dossier) { user.dossiers.first } - # TODO: check - # the order - # there are no extraneous input scenario 'fill a dossier', js: true, vcr: { cassette_name: 'api_geo_departements_regions_et_communes' } do log_in(user, procedure) @@ -160,6 +157,14 @@ feature 'The user' do create(:procedure, :published, :for_individual, types_de_champ: tdcs) end + let(:procedure_with_pjs) do + tdcs = [ + create(:type_de_champ_piece_justificative, mandatory: true, libelle: 'Pièce justificative 1', order_place: 1), + create(:type_de_champ_piece_justificative, mandatory: true, libelle: 'Pièce justificative 2', order_place: 2) + ] + create(:procedure, :published, :for_individual, types_de_champ: tdcs) + end + scenario 'adding, replacing and removing attachments', js: true do log_in(user, procedure_with_pj) fill_individual @@ -191,6 +196,85 @@ feature 'The user' do expect(page).to have_no_text('RIB.pdf') end + context 'when the auto-uploads of attachments is enabled' do + before do + Flipper.enable_actor(:autoupload_dossier_attachments, user) + end + + scenario 'add an attachment', js: true do + log_in(user, procedure_with_pjs) + fill_individual + + # Add attachments + find_field('Pièce justificative 1').attach_file(Rails.root + 'spec/fixtures/files/file.pdf') + find_field('Pièce justificative 2').attach_file(Rails.root + 'spec/fixtures/files/RIB.pdf') + + # Expect the files to be uploaded immediately + expect(page).to have_text('analyse antivirus en cours', count: 2) + expect(page).to have_text('file.pdf') + expect(page).to have_text('RIB.pdf') + + # Expect the submit buttons to be enabled + expect(page).to have_button('Enregistrer le brouillon', disabled: false) + expect(page).to have_button('Déposer le dossier', disabled: false) + + # Reload the current page + visit current_path + + # Expect the files to have been saved on the dossier + expect(page).to have_text('file.pdf') + expect(page).to have_text('RIB.pdf') + end + + # TODO: once we're running on Rails 6, re-enable the validator on PieceJustificativeChamp, + # and unmark this spec as pending. + # + # See piece_justificative_champ.rb + # See https://github.com/betagouv/demarches-simplifiees.fr/issues/4926 + scenario 'add an invalid attachment', js: true, pending: true do + log_in(user, procedure_with_pjs) + fill_individual + + # Test invalid file type + attach_file('Pièce justificative 1', Rails.root + 'spec/fixtures/files/invalid_file_format.json') + expect(page).to have_text('La pièce justificative n’est pas d’un type accepté') + expect(page).to have_no_button('Ré-essayer', visible: true) + + # Replace the file by another with a valid type + attach_file('Pièce justificative 1', Rails.root + 'spec/fixtures/files/piece_justificative_0.pdf') + expect(page).to have_no_text('La pièce justificative n’est pas d’un type accepté') + expect(page).to have_text('analyse antivirus en cours') + expect(page).to have_text('piece_justificative_0.pdf') + end + + scenario 'retry on transcient upload error', js: true do + log_in(user, procedure_with_pjs) + fill_individual + + # Test auto-upload failure + logout(:user) # Make the subsequent auto-upload request fail + attach_file('Pièce justificative 1', Rails.root + 'spec/fixtures/files/file.pdf') + expect(page).to have_text('Une erreur s’est produite pendant l’envoi du fichier') + expect(page).to have_button('Ré-essayer', visible: true) + expect(page).to have_button('Enregistrer le brouillon', disabled: false) + expect(page).to have_button('Déposer le dossier', disabled: false) + + # Test that retrying after a failure works + login_as(user, scope: :user) # Make the auto-upload request work again + click_on('Ré-essayer', visible: true) + expect(page).to have_text('analyse antivirus en cours') + expect(page).to have_text('file.pdf') + expect(page).to have_button('Enregistrer le brouillon', disabled: false) + expect(page).to have_button('Déposer le dossier', disabled: false) + + # Reload the current page + visit current_path + + # Expect the file to have been saved on the dossier + expect(page).to have_text('file.pdf') + end + end + context 'when the draft autosave is enabled' do before do Flipper.enable_actor(:autosave_dossier_draft, user) diff --git a/spec/fixtures/files/invalid_file_format.json b/spec/fixtures/files/invalid_file_format.json new file mode 100644 index 000000000..d3efb6fc9 --- /dev/null +++ b/spec/fixtures/files/invalid_file_format.json @@ -0,0 +1,3 @@ +{ + "text": "The format of this attachment is rejected by most uploaders." +}