Merge pull request #9643 from tchak/feat-autosave-validation
feat(dossier): validate on change and revalidate on input
This commit is contained in:
commit
13eadb93bf
32 changed files with 247 additions and 334 deletions
|
@ -61,7 +61,13 @@ module Administrateurs
|
||||||
|
|
||||||
def apercu
|
def apercu
|
||||||
@dossier = procedure_without_control.draft_revision.dossier_for_preview(current_user)
|
@dossier = procedure_without_control.draft_revision.dossier_for_preview(current_user)
|
||||||
|
DossierPreloader.load_one(@dossier)
|
||||||
@tab = apercu_tab
|
@tab = apercu_tab
|
||||||
|
if @tab == 'dossier'
|
||||||
|
@dossier.validate(:champs_public_value)
|
||||||
|
else
|
||||||
|
@dossier.validate(:champs_private_value)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
|
|
|
@ -292,9 +292,8 @@ module Instructeurs
|
||||||
if dossier.champs_private_all.any?(&:changed?)
|
if dossier.champs_private_all.any?(&:changed?)
|
||||||
dossier.last_champ_private_updated_at = Time.zone.now
|
dossier.last_champ_private_updated_at = Time.zone.now
|
||||||
end
|
end
|
||||||
if !dossier.save(context: :annotations)
|
|
||||||
flash.now.alert = dossier.errors.full_messages
|
dossier.save(context: :champs_private_value)
|
||||||
end
|
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.turbo_stream do
|
format.turbo_stream do
|
||||||
|
|
|
@ -204,7 +204,7 @@ module Users
|
||||||
session.delete(:prefill_token)
|
session.delete(:prefill_token)
|
||||||
session.delete(:prefill_params)
|
session.delete(:prefill_params)
|
||||||
@dossier = dossier_with_champs
|
@dossier = dossier_with_champs
|
||||||
@dossier.valid?(context: :prefilling)
|
@dossier.validate(:champs_public_value)
|
||||||
end
|
end
|
||||||
|
|
||||||
def submit_brouillon
|
def submit_brouillon
|
||||||
|
@ -519,27 +519,28 @@ module Users
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_dossier_and_compute_errors
|
def update_dossier_and_compute_errors
|
||||||
errors = []
|
|
||||||
|
|
||||||
@dossier.assign_attributes(champs_public_params)
|
@dossier.assign_attributes(champs_public_params)
|
||||||
if @dossier.champs_public_all.any?(&:changed_for_autosave?)
|
if @dossier.champs_public_all.any?(&:changed_for_autosave?)
|
||||||
@dossier.last_champ_updated_at = Time.zone.now
|
@dossier.last_champ_updated_at = Time.zone.now
|
||||||
end
|
end
|
||||||
|
|
||||||
if !@dossier.save(**validation_options)
|
# We save the dossier without validating fields, and if it is successful and the client
|
||||||
errors = @dossier.errors
|
# requests it, we ask for field validation errors.
|
||||||
|
if @dossier.save && params[:validate].present?
|
||||||
|
@dossier.valid?(:champs_public_value)
|
||||||
end
|
end
|
||||||
|
|
||||||
errors
|
@dossier.errors
|
||||||
end
|
end
|
||||||
|
|
||||||
def submit_dossier_and_compute_errors
|
def submit_dossier_and_compute_errors
|
||||||
@dossier.valid?(**submit_validation_options)
|
@dossier.validate(:champs_public_value)
|
||||||
|
|
||||||
errors = @dossier.errors
|
errors = @dossier.errors
|
||||||
@dossier.check_mandatory_and_visible_champs.map do |error_on_champ|
|
@dossier.check_mandatory_and_visible_champs.map do |error_on_champ|
|
||||||
errors.import(error_on_champ)
|
errors.import(error_on_champ)
|
||||||
end
|
end
|
||||||
|
|
||||||
errors
|
errors
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -588,20 +589,5 @@ module Users
|
||||||
def commentaire_params
|
def commentaire_params
|
||||||
params.require(:commentaire).permit(:body, :piece_jointe)
|
params.require(:commentaire).permit(:body, :piece_jointe)
|
||||||
end
|
end
|
||||||
|
|
||||||
def submit_validation_options
|
|
||||||
# rubocop:disable Lint/BooleanSymbol
|
|
||||||
# Force ActiveRecord to re-validate associated records.
|
|
||||||
{ context: :false }
|
|
||||||
# rubocop:enable Lint/BooleanSymbol
|
|
||||||
end
|
|
||||||
|
|
||||||
def validation_options
|
|
||||||
if dossier.brouillon?
|
|
||||||
{ context: :brouillon }
|
|
||||||
else
|
|
||||||
submit_validation_options
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
import {
|
import { httpRequest, ResponseError, getConfig } from '@utils';
|
||||||
httpRequest,
|
import { matchInputElement, isButtonElement } from '@coldwired/utils';
|
||||||
ResponseError,
|
|
||||||
isSelectElement,
|
|
||||||
isCheckboxOrRadioInputElement,
|
|
||||||
isTextInputElement,
|
|
||||||
getConfig
|
|
||||||
} from '@utils';
|
|
||||||
|
|
||||||
import { ApplicationController } from './application_controller';
|
import { ApplicationController } from './application_controller';
|
||||||
import { AutoUpload } from '../shared/activestorage/auto-upload';
|
import { AutoUpload } from '../shared/activestorage/auto-upload';
|
||||||
|
@ -54,66 +48,84 @@ export class AutosaveController extends ApplicationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickRetryButton(event: Event) {
|
onClickRetryButton(event: Event) {
|
||||||
const target = event.target as HTMLButtonElement;
|
const target = event.target;
|
||||||
const inputTargetSelector = target.dataset.inputTarget;
|
if (isButtonElement(target)) {
|
||||||
if (inputTargetSelector) {
|
const inputTargetSelector = target.dataset.inputTarget;
|
||||||
const target =
|
if (inputTargetSelector) {
|
||||||
this.element.querySelector<HTMLInputElement>(inputTargetSelector);
|
const target =
|
||||||
if (
|
this.element.querySelector<HTMLInputElement>(inputTargetSelector);
|
||||||
target &&
|
if (
|
||||||
target.type == 'file' &&
|
target &&
|
||||||
target.dataset.autoAttachUrl &&
|
target.type == 'file' &&
|
||||||
target.files?.length
|
target.dataset.autoAttachUrl &&
|
||||||
) {
|
target.files?.length
|
||||||
this.enqueueAutouploadRequest(target, target.files[0]);
|
) {
|
||||||
|
this.enqueueAutouploadRequest(target, target.files[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onChange(event: Event) {
|
private onChange(event: Event) {
|
||||||
const target = event.target as HTMLInputElement;
|
matchInputElement(event.target, {
|
||||||
if (!target.disabled) {
|
file: (target) => {
|
||||||
if (target.type == 'file') {
|
|
||||||
if (target.dataset.autoAttachUrl && target.files?.length) {
|
if (target.dataset.autoAttachUrl && target.files?.length) {
|
||||||
this.globalDispatch('autosave:input');
|
this.globalDispatch('autosave:input');
|
||||||
this.enqueueAutouploadRequest(target, target.files[0]);
|
this.enqueueAutouploadRequest(target, target.files[0]);
|
||||||
}
|
}
|
||||||
} else if (target.type == 'hidden') {
|
},
|
||||||
this.globalDispatch('autosave:input');
|
changeable: (target) => {
|
||||||
// In React comboboxes we dispatch a "change" event on hidden inputs to trigger autosave.
|
|
||||||
// We want to debounce them.
|
|
||||||
this.debounce(this.enqueueAutosaveRequest, AUTOSAVE_DEBOUNCE_DELAY);
|
|
||||||
} else if (
|
|
||||||
isSelectElement(target) ||
|
|
||||||
isCheckboxOrRadioInputElement(target)
|
|
||||||
) {
|
|
||||||
this.globalDispatch('autosave:input');
|
this.globalDispatch('autosave:input');
|
||||||
|
|
||||||
// Wait next tick so champs having JS can interact
|
// Wait next tick so champs having JS can interact
|
||||||
// with form elements before extracting form data.
|
// with form elements before extracting form data.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.enqueueAutosaveRequest();
|
this.enqueueAutosaveRequest();
|
||||||
this.showConditionnalSpinner(target);
|
this.showConditionnalSpinner(target);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
},
|
||||||
|
inputable: (target) => this.enqueueOnInput(target, true),
|
||||||
|
hidden: (target) => {
|
||||||
|
// In comboboxes we dispatch a "change" event on hidden inputs to trigger autosave.
|
||||||
|
// We want to debounce them.
|
||||||
|
this.enqueueOnInput(target, true);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onInput(event: Event) {
|
private onInput(event: Event) {
|
||||||
const target = event.target as HTMLInputElement;
|
matchInputElement(event.target, {
|
||||||
if (
|
inputable: (target) => {
|
||||||
!target.disabled &&
|
// Ignore input from React comboboxes. We trigger "change" events on them when selection is changed.
|
||||||
// Ignore input from React comboboxes. We trigger "change" events on them when selection is changed.
|
if (target.getAttribute('role') != 'combobox') {
|
||||||
target.getAttribute('role') != 'combobox' &&
|
const validate = this.needsValidation(target);
|
||||||
isTextInputElement(target)
|
this.enqueueOnInput(target, validate);
|
||||||
) {
|
}
|
||||||
this.globalDispatch('autosave:input');
|
}
|
||||||
this.debounce(this.enqueueAutosaveRequest, AUTOSAVE_DEBOUNCE_DELAY);
|
});
|
||||||
|
|
||||||
this.showConditionnalSpinner(target);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private showConditionnalSpinner(target: HTMLInputElement) {
|
private enqueueOnInput(
|
||||||
|
target: HTMLInputElement | HTMLTextAreaElement,
|
||||||
|
validate: boolean
|
||||||
|
) {
|
||||||
|
this.globalDispatch('autosave:input');
|
||||||
|
|
||||||
|
const callback = validate
|
||||||
|
? this.enqueueAutosaveWithValidationRequest
|
||||||
|
: this.enqueueAutosaveRequest;
|
||||||
|
this.debounce(callback, AUTOSAVE_DEBOUNCE_DELAY);
|
||||||
|
|
||||||
|
this.showConditionnalSpinner(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private needsValidation(target: HTMLElement) {
|
||||||
|
return target.getAttribute('aria-invalid') == 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
private showConditionnalSpinner(
|
||||||
|
target: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||||
|
) {
|
||||||
const champWrapperElement = target.closest(
|
const champWrapperElement = target.closest(
|
||||||
'.editable-champ[data-dependent-conditions]'
|
'.editable-champ[data-dependent-conditions]'
|
||||||
);
|
);
|
||||||
|
@ -198,9 +210,18 @@ export class AutosaveController extends ApplicationController {
|
||||||
this.didEnqueue();
|
this.didEnqueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enqueueAutosaveWithValidationRequest() {
|
||||||
|
this.#latestPromise = this.#latestPromise.finally(() =>
|
||||||
|
this.sendAutosaveRequest(true)
|
||||||
|
.then(() => this.didSucceed())
|
||||||
|
.catch((error) => this.didFail(error))
|
||||||
|
);
|
||||||
|
this.didEnqueue();
|
||||||
|
}
|
||||||
|
|
||||||
// Create a fetch request that saves the form.
|
// Create a fetch request that saves the form.
|
||||||
// Returns a promise fulfilled when the request completes.
|
// Returns a promise fulfilled when the request completes.
|
||||||
private sendAutosaveRequest(): Promise<void> {
|
private sendAutosaveRequest(validate = false): Promise<void> {
|
||||||
this.#abortController = new AbortController();
|
this.#abortController = new AbortController();
|
||||||
const { form, inputs } = this;
|
const { form, inputs } = this;
|
||||||
|
|
||||||
|
@ -222,6 +243,9 @@ export class AutosaveController extends ApplicationController {
|
||||||
formData.append(input.name, input.value);
|
formData.append(input.name, input.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (validate) {
|
||||||
|
formData.append('validate', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
this.#pendingPromiseCount++;
|
this.#pendingPromiseCount++;
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { isButtonElement } from '@utils';
|
import { isButtonElement } from '@coldwired/utils';
|
||||||
|
|
||||||
import { ApplicationController } from './application_controller';
|
import { ApplicationController } from './application_controller';
|
||||||
|
|
||||||
export class AutosaveSubmitController extends ApplicationController {
|
export class AutosaveSubmitController extends ApplicationController {
|
||||||
#isSaving = false;
|
#isSaving = false;
|
||||||
#shouldSubmit = true;
|
#shouldSubmit = false;
|
||||||
#buttonText?: string;
|
#buttonText?: string;
|
||||||
|
|
||||||
connect(): void {
|
connect(): void {
|
||||||
|
@ -46,15 +46,20 @@ export class AutosaveSubmitController extends ApplicationController {
|
||||||
|
|
||||||
private disableButton() {
|
private disableButton() {
|
||||||
if (isButtonElement(this.element)) {
|
if (isButtonElement(this.element)) {
|
||||||
this.#buttonText = this.element.value;
|
const disableWith = this.element.dataset.disableWith ?? '';
|
||||||
this.element.value = this.element.dataset.disableWith ?? '';
|
if (disableWith) {
|
||||||
|
this.#buttonText = this.element.textContent ?? undefined;
|
||||||
|
this.element.textContent = disableWith;
|
||||||
|
}
|
||||||
this.element.disabled = true;
|
this.element.disabled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enableButton() {
|
private enableButton() {
|
||||||
if (isButtonElement(this.element) && this.#buttonText) {
|
if (isButtonElement(this.element)) {
|
||||||
this.element.value = this.#buttonText;
|
if (this.#buttonText) {
|
||||||
|
this.element.textContent = this.#buttonText;
|
||||||
|
}
|
||||||
this.element.disabled = false;
|
this.element.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
import {
|
import { isFormInputElement, matchInputElement } from '@coldwired/utils';
|
||||||
isSelectElement,
|
|
||||||
isCheckboxOrRadioInputElement,
|
|
||||||
isTextInputElement,
|
|
||||||
isDateInputElement
|
|
||||||
} from '@utils';
|
|
||||||
import { isFormInputElement } from '@coldwired/utils';
|
|
||||||
|
|
||||||
import { ApplicationController } from './application_controller';
|
import { ApplicationController } from './application_controller';
|
||||||
|
|
||||||
|
@ -28,14 +22,9 @@ export class AutosubmitController extends ApplicationController {
|
||||||
|
|
||||||
private onChange(event: Event) {
|
private onChange(event: Event) {
|
||||||
const target = this.findTargetElement(event);
|
const target = this.findTargetElement(event);
|
||||||
if (!target) return;
|
|
||||||
|
|
||||||
if (
|
matchInputElement(target, {
|
||||||
isSelectElement(target) ||
|
date: (target) => {
|
||||||
isCheckboxOrRadioInputElement(target) ||
|
|
||||||
isTextInputElement(target)
|
|
||||||
) {
|
|
||||||
if (isDateInputElement(target)) {
|
|
||||||
if (target.value.trim() == '' || !isNaN(Date.parse(target.value))) {
|
if (target.value.trim() == '' || !isNaN(Date.parse(target.value))) {
|
||||||
this.#dateTimeChangedInputs.add(target);
|
this.#dateTimeChangedInputs.add(target);
|
||||||
this.debounce(this.submit, AUTOSUBMIT_DATE_DEBOUNCE_DELAY);
|
this.debounce(this.submit, AUTOSUBMIT_DATE_DEBOUNCE_DELAY);
|
||||||
|
@ -43,34 +32,34 @@ export class AutosubmitController extends ApplicationController {
|
||||||
this.#dateTimeChangedInputs.delete(target);
|
this.#dateTimeChangedInputs.delete(target);
|
||||||
this.cancelDebounce(this.submit);
|
this.cancelDebounce(this.submit);
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
this.cancelDebounce(this.submit);
|
text: () => this.submitNow(),
|
||||||
this.submit();
|
changeable: () => this.submitNow()
|
||||||
}
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onInput(event: Event) {
|
private onInput(event: Event) {
|
||||||
const target = this.findTargetElement(event);
|
const target = this.findTargetElement(event);
|
||||||
if (!target) return;
|
|
||||||
|
|
||||||
if (!isDateInputElement(target) && isTextInputElement(target)) {
|
matchInputElement(target, {
|
||||||
this.debounce(this.submit, AUTOSUBMIT_DEBOUNCE_DELAY);
|
date: () => {},
|
||||||
}
|
inputable: () => this.debounce(this.submit, AUTOSUBMIT_DEBOUNCE_DELAY)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onBlur(event: Event) {
|
private onBlur(event: Event) {
|
||||||
const target = this.findTargetElement(event);
|
const target = this.findTargetElement(event);
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
if (isDateInputElement(target)) {
|
matchInputElement(target, {
|
||||||
Promise.resolve().then(() => {
|
date: () => {
|
||||||
if (this.#dateTimeChangedInputs.has(target)) {
|
Promise.resolve().then(() => {
|
||||||
this.cancelDebounce(this.submit);
|
if (this.#dateTimeChangedInputs.has(target)) {
|
||||||
this.submit();
|
this.submitNow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private findTargetElement(event: Event) {
|
private findTargetElement(event: Event) {
|
||||||
|
@ -111,6 +100,11 @@ export class AutosubmitController extends ApplicationController {
|
||||||
return eventTypes.length == 0 ? true : eventTypes;
|
return eventTypes.length == 0 ? true : eventTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private submitNow() {
|
||||||
|
this.cancelDebounce(this.submit);
|
||||||
|
this.submit();
|
||||||
|
}
|
||||||
|
|
||||||
private submit() {
|
private submit() {
|
||||||
const submitter = this.hasSubmitterTarget ? this.submitterTarget : null;
|
const submitter = this.hasSubmitterTarget ? this.submitterTarget : null;
|
||||||
const form =
|
const form =
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { ActionEvent } from '@hotwired/stimulus';
|
import { ActionEvent } from '@hotwired/stimulus';
|
||||||
import {
|
import { httpRequest, getConfig } from '@utils';
|
||||||
httpRequest,
|
import { matchInputElement } from '@coldwired/utils';
|
||||||
isSelectElement,
|
|
||||||
isCheckboxOrRadioInputElement,
|
|
||||||
isTextInputElement,
|
|
||||||
getConfig
|
|
||||||
} from '@utils';
|
|
||||||
import { AutoUpload } from '../shared/activestorage/auto-upload';
|
import { AutoUpload } from '../shared/activestorage/auto-upload';
|
||||||
import { ApplicationController } from './application_controller';
|
import { ApplicationController } from './application_controller';
|
||||||
|
|
||||||
|
@ -59,29 +55,26 @@ export class TypeDeChampEditorController extends ApplicationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onChange(event: Event) {
|
private onChange(event: Event) {
|
||||||
const target = event.target as HTMLElement & { form?: HTMLFormElement };
|
matchInputElement(event.target, {
|
||||||
|
file: (target) => {
|
||||||
if (
|
if (target.files?.length) {
|
||||||
target.form &&
|
const autoupload = new AutoUpload(target, target.files[0]);
|
||||||
(isSelectElement(target) || isCheckboxOrRadioInputElement(target))
|
autoupload.start();
|
||||||
) {
|
}
|
||||||
this.save(target.form);
|
},
|
||||||
}
|
changeable: (target) => this.save(target.form)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onInput(event: Event) {
|
private onInput(event: Event) {
|
||||||
const target = event.target as HTMLInputElement;
|
matchInputElement(event.target, {
|
||||||
|
inputable: (target) => {
|
||||||
// mark input as touched so we know to not overwrite it's value with next re-render
|
if (target.form) {
|
||||||
target.setAttribute('data-touched', 'true');
|
this.#dirtyForms.add(target.form);
|
||||||
|
this.debounce(this.save, AUTOSAVE_DEBOUNCE_DELAY);
|
||||||
if (target.form && isTextInputElement(target)) {
|
}
|
||||||
this.#dirtyForms.add(target.form);
|
}
|
||||||
this.debounce(this.save, AUTOSAVE_DEBOUNCE_DELAY);
|
});
|
||||||
} else if (target.form && target.type == 'file' && target.files?.length) {
|
|
||||||
const autoupload = new AutoUpload(target, target.files[0]);
|
|
||||||
autoupload.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSortableEnd(event: CustomEvent<{ position: number }>) {
|
private onSortableEnd(event: CustomEvent<{ position: number }>) {
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
import { suite, test, expect } from 'vitest';
|
import { suite, test, expect } from 'vitest';
|
||||||
|
|
||||||
import {
|
import { show, hide, toggle, toggleExpandIcon } from './utils';
|
||||||
show,
|
|
||||||
hide,
|
|
||||||
toggle,
|
|
||||||
toggleExpandIcon,
|
|
||||||
isSelectElement,
|
|
||||||
isTextInputElement,
|
|
||||||
isCheckboxOrRadioInputElement
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
suite('@utils', () => {
|
suite('@utils', () => {
|
||||||
test('show', () => {
|
test('show', () => {
|
||||||
|
@ -46,79 +38,4 @@ suite('@utils', () => {
|
||||||
expect(icon.classList.contains('fr-icon-arrow-down-s-line')).toBeTruthy();
|
expect(icon.classList.contains('fr-icon-arrow-down-s-line')).toBeTruthy();
|
||||||
expect(icon.classList.contains('fr-icon-arrow-up-s-line')).toBeFalsy();
|
expect(icon.classList.contains('fr-icon-arrow-up-s-line')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isSelectElement', () => {
|
|
||||||
const select = document.createElement('select');
|
|
||||||
const input = document.createElement('input');
|
|
||||||
const textarea = document.createElement('textarea');
|
|
||||||
|
|
||||||
expect(isSelectElement(select)).toBeTruthy();
|
|
||||||
expect(isSelectElement(input)).toBeFalsy();
|
|
||||||
expect(isSelectElement(textarea)).toBeFalsy();
|
|
||||||
|
|
||||||
input.type = 'text';
|
|
||||||
expect(isSelectElement(input)).toBeFalsy();
|
|
||||||
|
|
||||||
input.type = 'email';
|
|
||||||
expect(isSelectElement(input)).toBeFalsy();
|
|
||||||
|
|
||||||
input.type = 'checkbox';
|
|
||||||
expect(isSelectElement(input)).toBeFalsy();
|
|
||||||
|
|
||||||
input.type = 'radio';
|
|
||||||
expect(isSelectElement(input)).toBeFalsy();
|
|
||||||
|
|
||||||
input.type = 'file';
|
|
||||||
expect(isSelectElement(input)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('isTextInputElement', () => {
|
|
||||||
const select = document.createElement('select');
|
|
||||||
const input = document.createElement('input');
|
|
||||||
const textarea = document.createElement('textarea');
|
|
||||||
|
|
||||||
expect(isTextInputElement(select)).toBeFalsy();
|
|
||||||
expect(isTextInputElement(input)).toBeTruthy();
|
|
||||||
expect(isTextInputElement(textarea)).toBeTruthy();
|
|
||||||
|
|
||||||
input.type = 'text';
|
|
||||||
expect(isTextInputElement(input)).toBeTruthy();
|
|
||||||
|
|
||||||
input.type = 'email';
|
|
||||||
expect(isTextInputElement(input)).toBeTruthy();
|
|
||||||
|
|
||||||
input.type = 'checkbox';
|
|
||||||
expect(isTextInputElement(input)).toBeFalsy();
|
|
||||||
|
|
||||||
input.type = 'radio';
|
|
||||||
expect(isTextInputElement(input)).toBeFalsy();
|
|
||||||
|
|
||||||
input.type = 'file';
|
|
||||||
expect(isTextInputElement(input)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('isCheckboxOrRadioInputElement', () => {
|
|
||||||
const select = document.createElement('select');
|
|
||||||
const input = document.createElement('input');
|
|
||||||
const textarea = document.createElement('textarea');
|
|
||||||
|
|
||||||
expect(isCheckboxOrRadioInputElement(select)).toBeFalsy();
|
|
||||||
expect(isCheckboxOrRadioInputElement(input)).toBeFalsy();
|
|
||||||
expect(isCheckboxOrRadioInputElement(textarea)).toBeFalsy();
|
|
||||||
|
|
||||||
input.type = 'text';
|
|
||||||
expect(isCheckboxOrRadioInputElement(input)).toBeFalsy();
|
|
||||||
|
|
||||||
input.type = 'email';
|
|
||||||
expect(isCheckboxOrRadioInputElement(input)).toBeFalsy();
|
|
||||||
|
|
||||||
input.type = 'checkbox';
|
|
||||||
expect(isCheckboxOrRadioInputElement(input)).toBeTruthy();
|
|
||||||
|
|
||||||
input.type = 'radio';
|
|
||||||
expect(isCheckboxOrRadioInputElement(input)).toBeTruthy();
|
|
||||||
|
|
||||||
input.type = 'file';
|
|
||||||
expect(isCheckboxOrRadioInputElement(input)).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -269,50 +269,6 @@ export function isNumeric(s: string) {
|
||||||
return !isNaN(n) && isFinite(n);
|
return !isNaN(n) && isFinite(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isButtonElement(
|
|
||||||
element: Element
|
|
||||||
): element is HTMLButtonElement {
|
|
||||||
return (
|
|
||||||
element.tagName == 'BUTTON' ||
|
|
||||||
(element.tagName == 'INPUT' &&
|
|
||||||
(element as HTMLInputElement).type == 'submit')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSelectElement(
|
|
||||||
element: HTMLElement
|
|
||||||
): element is HTMLSelectElement {
|
|
||||||
return element.tagName == 'SELECT';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isCheckboxOrRadioInputElement(
|
|
||||||
element: HTMLElement & { type?: string }
|
|
||||||
): element is HTMLInputElement {
|
|
||||||
return (
|
|
||||||
element.tagName == 'INPUT' &&
|
|
||||||
(element.type == 'checkbox' || element.type == 'radio')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isDateInputElement(
|
|
||||||
element: HTMLElement & { type?: string }
|
|
||||||
): element is HTMLInputElement {
|
|
||||||
return (
|
|
||||||
element.tagName == 'INPUT' &&
|
|
||||||
(element.type == 'date' || element.type == 'datetime-local')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isTextInputElement(
|
|
||||||
element: HTMLElement & { type?: string }
|
|
||||||
): element is HTMLInputElement {
|
|
||||||
return (
|
|
||||||
['INPUT', 'TEXTAREA'].includes(element.tagName) &&
|
|
||||||
typeof element.type == 'string' &&
|
|
||||||
!['checkbox', 'radio', 'file'].includes(element.type)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fire<T>(obj: EventTarget, name: string, data?: T) {
|
export function fire<T>(obj: EventTarget, name: string, data?: T) {
|
||||||
const event = new CustomEvent(name, {
|
const event = new CustomEvent(name, {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
|
|
|
@ -250,8 +250,25 @@ class Champ < ApplicationRecord
|
||||||
public? && dossier.champ_forked_with_changes?(self)
|
public? && dossier.champ_forked_with_changes?(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def valid_champ_value?
|
||||||
|
valid?(public? ? :champs_public_value : :champs_private_value)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def validate_champ_value?
|
||||||
|
case validation_context
|
||||||
|
when :champs_public_value
|
||||||
|
public?
|
||||||
|
when :champs_private_value
|
||||||
|
private?
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def html_id
|
def html_id
|
||||||
"champ-#{stable_id}-#{id}"
|
"champ-#{stable_id}-#{id}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class Champs::CnafChamp < Champs::TextChamp
|
class Champs::CnafChamp < Champs::TextChamp
|
||||||
# see https://github.com/betagouv/api-particulier/blob/master/src/presentation/middlewares/cnaf-input-validation.middleware.ts
|
# see https://github.com/betagouv/api-particulier/blob/master/src/presentation/middlewares/cnaf-input-validation.middleware.ts
|
||||||
validates :numero_allocataire, format: { with: /\A\d{1,7}\z/ }, if: -> { code_postal.present? && validation_context != :brouillon }
|
validates :numero_allocataire, format: { with: /\A\d{1,7}\z/ }, if: -> { code_postal.present? && validate_champ_value? }
|
||||||
validates :code_postal, format: { with: /\A\w{5}\z/ }, if: -> { numero_allocataire.present? && validation_context != :brouillon }
|
validates :code_postal, format: { with: /\A\w{5}\z/ }, if: -> { numero_allocataire.present? && validate_champ_value? }
|
||||||
|
|
||||||
store_accessor :value_json, :numero_allocataire, :code_postal
|
store_accessor :value_json, :numero_allocataire, :code_postal
|
||||||
|
|
||||||
|
@ -14,14 +14,14 @@ class Champs::CnafChamp < Champs::TextChamp
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_external_data
|
def fetch_external_data
|
||||||
if valid?
|
return unless valid_champ_value?
|
||||||
APIParticulier::CnafAdapter.new(
|
|
||||||
procedure.api_particulier_token,
|
APIParticulier::CnafAdapter.new(
|
||||||
numero_allocataire,
|
procedure.api_particulier_token,
|
||||||
code_postal,
|
numero_allocataire,
|
||||||
procedure.api_particulier_sources
|
code_postal,
|
||||||
).to_params
|
procedure.api_particulier_sources
|
||||||
end
|
).to_params
|
||||||
end
|
end
|
||||||
|
|
||||||
def external_id
|
def external_id
|
||||||
|
|
|
@ -25,7 +25,7 @@ class Champs::DecimalNumberChamp < Champ
|
||||||
end
|
end
|
||||||
|
|
||||||
def processed_value
|
def processed_value
|
||||||
return if invalid?
|
return unless valid_champ_value?
|
||||||
|
|
||||||
value&.to_f
|
value&.to_f
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class Champs::DgfipChamp < Champs::TextChamp
|
class Champs::DgfipChamp < Champs::TextChamp
|
||||||
# see https://github.com/betagouv/api-particulier/blob/master/src/presentation/middlewares/dgfip-input-validation.middleware.ts
|
# see https://github.com/betagouv/api-particulier/blob/master/src/presentation/middlewares/dgfip-input-validation.middleware.ts
|
||||||
validates :numero_fiscal, format: { with: /\A\w{13,14}\z/ }, if: -> { reference_avis.present? && validation_context != :brouillon }
|
validates :numero_fiscal, format: { with: /\A\w{13,14}\z/ }, if: -> { reference_avis.present? && validate_champ_value? }
|
||||||
validates :reference_avis, format: { with: /\A\w{13,14}\z/ }, if: -> { numero_fiscal.present? && validation_context != :brouillon }
|
validates :reference_avis, format: { with: /\A\w{13,14}\z/ }, if: -> { numero_fiscal.present? && validate_champ_value? }
|
||||||
|
|
||||||
store_accessor :value_json, :numero_fiscal, :reference_avis
|
store_accessor :value_json, :numero_fiscal, :reference_avis
|
||||||
|
|
||||||
|
@ -14,14 +14,14 @@ class Champs::DgfipChamp < Champs::TextChamp
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_external_data
|
def fetch_external_data
|
||||||
if valid?
|
return unless valid_champ_value?
|
||||||
APIParticulier::DgfipAdapter.new(
|
|
||||||
procedure.api_particulier_token,
|
APIParticulier::DgfipAdapter.new(
|
||||||
numero_fiscal,
|
procedure.api_particulier_token,
|
||||||
reference_avis,
|
numero_fiscal,
|
||||||
procedure.api_particulier_sources
|
reference_avis,
|
||||||
).to_params
|
procedure.api_particulier_sources
|
||||||
end
|
).to_params
|
||||||
end
|
end
|
||||||
|
|
||||||
def external_id
|
def external_id
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
class Champs::ExpressionReguliereChamp < Champ
|
class Champs::ExpressionReguliereChamp < Champ
|
||||||
validates_with ExpressionReguliereValidator, if: -> { validation_context != :brouillon }
|
validates_with ExpressionReguliereValidator, if: :validate_champ_value?
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
class Champs::IbanChamp < Champ
|
class Champs::IbanChamp < Champ
|
||||||
validates_with IbanValidator, if: -> { validation_context != :brouillon }
|
validates_with IbanValidator, if: :validate_champ_value?
|
||||||
after_validation :format_iban
|
after_validation :format_iban
|
||||||
|
|
||||||
def for_api
|
def for_api
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Champs::IntegerNumberChamp < Champ
|
||||||
private
|
private
|
||||||
|
|
||||||
def processed_value
|
def processed_value
|
||||||
return if invalid?
|
return unless valid_champ_value?
|
||||||
|
|
||||||
value&.to_i
|
value&.to_i
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,7 @@ class Champs::MesriChamp < Champs::TextChamp
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_external_data
|
def fetch_external_data
|
||||||
return if !valid?
|
return unless valid_champ_value?
|
||||||
|
|
||||||
APIParticulier::MesriAdapter.new(
|
APIParticulier::MesriAdapter.new(
|
||||||
procedure.api_particulier_token,
|
procedure.api_particulier_token,
|
||||||
|
|
|
@ -11,7 +11,7 @@ class Champs::PoleEmploiChamp < Champs::TextChamp
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_external_data
|
def fetch_external_data
|
||||||
return if !valid?
|
return unless valid_champ_value?
|
||||||
|
|
||||||
APIParticulier::PoleEmploiAdapter.new(
|
APIParticulier::PoleEmploiAdapter.new(
|
||||||
procedure.api_particulier_token,
|
procedure.api_particulier_token,
|
||||||
|
|
|
@ -3,7 +3,7 @@ class Champs::RNAChamp < Champ
|
||||||
|
|
||||||
validates :value, allow_blank: true, format: {
|
validates :value, allow_blank: true, format: {
|
||||||
with: /\AW[0-9]{9}\z/, message: I18n.t(:not_a_rna, scope: 'activerecord.errors.messages')
|
with: /\AW[0-9]{9}\z/, message: I18n.t(:not_a_rna, scope: 'activerecord.errors.messages')
|
||||||
}, if: -> { validation_context != :brouillon }
|
}, if: :validate_champ_value?
|
||||||
|
|
||||||
delegate :id, to: :procedure, prefix: true
|
delegate :id, to: :procedure, prefix: true
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ module RNAChampAssociationFetchableConcern
|
||||||
self.value = rna
|
self.value = rna
|
||||||
|
|
||||||
return clear_association!(:empty) if rna.empty?
|
return clear_association!(:empty) if rna.empty?
|
||||||
return clear_association!(:invalid) unless valid?
|
return clear_association!(:invalid) unless valid?(:champs_public_value)
|
||||||
return clear_association!(:not_found) if (data = APIEntreprise::RNAAdapter.new(rna, procedure_id).to_params).blank?
|
return clear_association!(:not_found) if (data = APIEntreprise::RNAAdapter.new(rna, procedure_id).to_params).blank?
|
||||||
|
|
||||||
update!(data: data)
|
update!(data: data)
|
||||||
|
@ -21,7 +21,7 @@ module RNAChampAssociationFetchableConcern
|
||||||
def clear_association!(error)
|
def clear_association!(error)
|
||||||
@association_fetch_error_key = error
|
@association_fetch_error_key = error
|
||||||
self.data = nil
|
self.data = nil
|
||||||
save!(context: :brouillon)
|
save!
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -440,7 +440,7 @@ class Dossier < ApplicationRecord
|
||||||
validates :user, presence: true, if: -> { deleted_user_email_never_send.nil? }, unless: -> { prefilled }
|
validates :user, presence: true, if: -> { deleted_user_email_never_send.nil? }, unless: -> { prefilled }
|
||||||
validates :individual, presence: true, if: -> { revision.procedure.for_individual? }
|
validates :individual, presence: true, if: -> { revision.procedure.for_individual? }
|
||||||
|
|
||||||
validates_associated :prefilled_champs_public, on: :prefilling
|
validates_associated :prefilled_champs_public, on: :champs_public_value
|
||||||
|
|
||||||
def types_de_champ_public
|
def types_de_champ_public
|
||||||
types_de_champ
|
types_de_champ
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coldwired/actions": "^0.11.2",
|
"@coldwired/actions": "^0.11.2",
|
||||||
"@coldwired/turbo-stream": "^0.11.1",
|
"@coldwired/turbo-stream": "^0.11.1",
|
||||||
"@coldwired/utils": "^0.11.1",
|
"@coldwired/utils": "^0.11.4",
|
||||||
"@gouvfr/dsfr": "^1.10.1",
|
"@gouvfr/dsfr": "^1.10.1",
|
||||||
"@graphiql/plugin-explorer": "^0.3.4",
|
"@graphiql/plugin-explorer": "^0.3.4",
|
||||||
"@graphiql/toolkit": "^0.9.1",
|
"@graphiql/toolkit": "^0.9.1",
|
||||||
|
|
|
@ -411,7 +411,7 @@ describe Users::DossiersController, type: :controller do
|
||||||
render_views
|
render_views
|
||||||
let(:error_message) { 'nop' }
|
let(:error_message) { 'nop' }
|
||||||
before do
|
before do
|
||||||
expect_any_instance_of(Dossier).to receive(:valid?).and_return(false)
|
expect_any_instance_of(Dossier).to receive(:validate).and_return(false)
|
||||||
expect_any_instance_of(Dossier).to receive(:errors).and_return(
|
expect_any_instance_of(Dossier).to receive(:errors).and_return(
|
||||||
[double(inner_error: double(base: first_champ), message: 'nop')]
|
[double(inner_error: double(base: first_champ), message: 'nop')]
|
||||||
)
|
)
|
||||||
|
@ -518,7 +518,7 @@ describe Users::DossiersController, type: :controller do
|
||||||
render_views
|
render_views
|
||||||
|
|
||||||
before do
|
before do
|
||||||
expect_any_instance_of(Dossier).to receive(:valid?).and_return(false)
|
expect_any_instance_of(Dossier).to receive(:validate).and_return(false)
|
||||||
expect_any_instance_of(Dossier).to receive(:errors).and_return(
|
expect_any_instance_of(Dossier).to receive(:errors).and_return(
|
||||||
[double(inner_error: double(base: first_champ), message: 'nop')]
|
[double(inner_error: double(base: first_champ), message: 'nop')]
|
||||||
)
|
)
|
||||||
|
|
|
@ -38,7 +38,7 @@ describe Champs::CnafChamp, type: :model do
|
||||||
let(:numero_allocataire) { '1234567' }
|
let(:numero_allocataire) { '1234567' }
|
||||||
let(:code_postal) { '12345' }
|
let(:code_postal) { '12345' }
|
||||||
let(:champ) { described_class.new(dossier: create(:dossier), type_de_champ: create(:type_de_champ_cnaf)) }
|
let(:champ) { described_class.new(dossier: create(:dossier), type_de_champ: create(:type_de_champ_cnaf)) }
|
||||||
let(:validation_context) { :create }
|
let(:validation_context) { :champs_public_value }
|
||||||
|
|
||||||
subject { champ.valid?(validation_context) }
|
subject { champ.valid?(validation_context) }
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ describe Champs::DgfipChamp, type: :model do
|
||||||
let(:numero_fiscal) { '1122299999092' }
|
let(:numero_fiscal) { '1122299999092' }
|
||||||
let(:reference_avis) { 'FC22299999092' }
|
let(:reference_avis) { 'FC22299999092' }
|
||||||
let(:champ) { described_class.new(dossier: create(:dossier), type_de_champ: create(:type_de_champ_dgfip)) }
|
let(:champ) { described_class.new(dossier: create(:dossier), type_de_champ: create(:type_de_champ_dgfip)) }
|
||||||
let(:validation_context) { :create }
|
let(:validation_context) { :champs_public_value }
|
||||||
|
|
||||||
subject { champ.valid?(validation_context) }
|
subject { champ.valid?(validation_context) }
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
describe Champs::IbanChamp do
|
describe Champs::IbanChamp do
|
||||||
describe '#valid?' do
|
describe '#valid?' do
|
||||||
it do
|
it do
|
||||||
expect(build(:champ_iban, value: nil)).to be_valid
|
expect(build(:champ_iban, value: nil).valid?(:champs_public_value)).to be_truthy
|
||||||
expect(build(:champ_iban, value: "FR35 KDSQFDJQSMFDQMFDQ")).to_not be_valid
|
expect(build(:champ_iban, value: "FR35 KDSQFDJQSMFDQMFDQ").valid?(:champs_public_value)).to be_falsey
|
||||||
expect(build(:champ_iban, value: "FR7630006000011234567890189")).to be_valid
|
expect(build(:champ_iban, value: "FR7630006000011234567890189").valid?(:champs_public_value)).to be_truthy
|
||||||
expect(build(:champ_iban, value: "FR76 3000 6000 0112 3456 7890 189")).to be_valid
|
expect(build(:champ_iban, value: "FR76 3000 6000 0112 3456 7890 189").valid?(:champs_public_value)).to be_truthy
|
||||||
expect(build(:champ_iban, value: "FR76 3000 6000 0112 3456 7890 189DSF")).to_not be_valid
|
expect(build(:champ_iban, value: "FR76 3000 6000 0112 3456 7890 189DSF").valid?(:champs_public_value)).to be_falsey
|
||||||
expect(build(:champ_iban, value: "FR76 3000 6000 0112 3456 7890 189")).to be_valid
|
expect(build(:champ_iban, value: "FR76 3000 6000 0112 3456 7890 189").valid?(:champs_public_value)).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'format value after validation' do
|
it 'format value after validation' do
|
||||||
champ = build(:champ_iban, value: "FR76 3000 6000 0112 3456 7890 189")
|
champ = build(:champ_iban, value: "FR76 3000 6000 0112 3456 7890 189")
|
||||||
champ.valid?
|
champ.valid?(:champs_public_value)
|
||||||
expect(champ.value).to eq("FR76 3000 6000 0112 3456 7890 189")
|
expect(champ.value).to eq("FR76 3000 6000 0112 3456 7890 189")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,11 +2,11 @@ describe Champs::RNAChamp do
|
||||||
let(:champ) { create(:champ_rna, value: "W182736273") }
|
let(:champ) { create(:champ_rna, value: "W182736273") }
|
||||||
|
|
||||||
describe '#valid?' do
|
describe '#valid?' do
|
||||||
it { expect(build(:champ_rna, value: nil)).to be_valid }
|
it { expect(build(:champ_rna, value: nil).valid?(:champs_public_value)).to be_truthy }
|
||||||
it { expect(build(:champ_rna, value: "2736251627")).to_not be_valid }
|
it { expect(build(:champ_rna, value: "2736251627").valid?(:champs_public_value)).to be_falsey }
|
||||||
it { expect(build(:champ_rna, value: "A172736283")).to_not be_valid }
|
it { expect(build(:champ_rna, value: "A172736283").valid?(:champs_public_value)).to be_falsey }
|
||||||
it { expect(build(:champ_rna, value: "W1827362718")).to_not be_valid }
|
it { expect(build(:champ_rna, value: "W1827362718").valid?(:champs_public_value)).to be_falsey }
|
||||||
it { expect(build(:champ_rna, value: "W182736273")).to be_valid }
|
it { expect(build(:champ_rna, value: "W182736273").valid?(:champs_public_value)).to be_truthy }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#export" do
|
describe "#export" do
|
||||||
|
|
|
@ -1589,7 +1589,7 @@ describe Dossier, type: :model do
|
||||||
before do
|
before do
|
||||||
champ = dossier.champs_public.first
|
champ = dossier.champs_public.first
|
||||||
champ.value = expression_reguliere_exemple_text
|
champ.value = expression_reguliere_exemple_text
|
||||||
dossier.save
|
dossier.save(context: :champs_public_value)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should have errors' do
|
it 'should have errors' do
|
||||||
|
|
|
@ -262,7 +262,7 @@ describe 'fetch API Particulier Data', js: true, retry: 3 do
|
||||||
before { login_as user, scope: :user }
|
before { login_as user, scope: :user }
|
||||||
|
|
||||||
context 'CNAF' do
|
context 'CNAF' do
|
||||||
scenario 'it can fill an cnaf champ' do
|
scenario 'it can fill an cnaf champ', vcr: { cassette_name: 'api_particulier/success/composition_familiale' } do
|
||||||
visit commencer_path(path: procedure.path)
|
visit commencer_path(path: procedure.path)
|
||||||
click_on 'Commencer la démarche'
|
click_on 'Commencer la démarche'
|
||||||
|
|
||||||
|
@ -274,7 +274,6 @@ describe 'fetch API Particulier Data', js: true, retry: 3 do
|
||||||
|
|
||||||
fill_in 'Le numéro d’allocataire CAF', with: numero_allocataire
|
fill_in 'Le numéro d’allocataire CAF', with: numero_allocataire
|
||||||
fill_in 'Le code postal', with: 'wrong_code'
|
fill_in 'Le code postal', with: 'wrong_code'
|
||||||
wait_for_autosave
|
|
||||||
|
|
||||||
dossier = Dossier.last
|
dossier = Dossier.last
|
||||||
cnaf_champ = dossier.champs_public.find(&:cnaf?)
|
cnaf_champ = dossier.champs_public.find(&:cnaf?)
|
||||||
|
@ -284,12 +283,12 @@ describe 'fetch API Particulier Data', js: true, retry: 3 do
|
||||||
click_on 'Déposer le dossier'
|
click_on 'Déposer le dossier'
|
||||||
expect(page).to have_content("cnaf doit posséder 5 caractères")
|
expect(page).to have_content("cnaf doit posséder 5 caractères")
|
||||||
|
|
||||||
VCR.use_cassette('api_particulier/success/composition_familiale') do
|
fill_in 'Le code postal', with: code_postal
|
||||||
fill_in 'Le code postal', with: code_postal
|
wait_until { cnaf_champ.reload.external_id.present? }
|
||||||
wait_for_autosave
|
|
||||||
click_on 'Déposer le dossier'
|
click_on 'Déposer le dossier'
|
||||||
perform_enqueued_jobs
|
perform_enqueued_jobs
|
||||||
end
|
|
||||||
expect(page).to have_current_path(merci_dossier_path(Dossier.last))
|
expect(page).to have_current_path(merci_dossier_path(Dossier.last))
|
||||||
|
|
||||||
perform_enqueued_jobs
|
perform_enqueued_jobs
|
||||||
|
@ -321,7 +320,7 @@ describe 'fetch API Particulier Data', js: true, retry: 3 do
|
||||||
context 'Pôle emploi' do
|
context 'Pôle emploi' do
|
||||||
let(:api_particulier_token) { '06fd8675601267d2988cbbdef56ecb0de1d45223' }
|
let(:api_particulier_token) { '06fd8675601267d2988cbbdef56ecb0de1d45223' }
|
||||||
|
|
||||||
scenario 'it can fill a Pôle emploi field' do
|
scenario 'it can fill a Pôle emploi field', vcr: { cassette_name: 'api_particulier/success/situation_pole_emploi' } do
|
||||||
visit commencer_path(path: procedure.path)
|
visit commencer_path(path: procedure.path)
|
||||||
click_on 'Commencer la démarche'
|
click_on 'Commencer la démarche'
|
||||||
|
|
||||||
|
@ -332,7 +331,6 @@ describe 'fetch API Particulier Data', js: true, retry: 3 do
|
||||||
click_button('Continuer')
|
click_button('Continuer')
|
||||||
|
|
||||||
fill_in "Identifiant", with: 'wrong code'
|
fill_in "Identifiant", with: 'wrong code'
|
||||||
wait_for_autosave
|
|
||||||
|
|
||||||
dossier = Dossier.last
|
dossier = Dossier.last
|
||||||
pole_emploi_champ = dossier.champs_public.find(&:pole_emploi?)
|
pole_emploi_champ = dossier.champs_public.find(&:pole_emploi?)
|
||||||
|
@ -342,12 +340,12 @@ describe 'fetch API Particulier Data', js: true, retry: 3 do
|
||||||
clear_enqueued_jobs
|
clear_enqueued_jobs
|
||||||
pole_emploi_champ.update(external_id: nil, identifiant: nil)
|
pole_emploi_champ.update(external_id: nil, identifiant: nil)
|
||||||
|
|
||||||
VCR.use_cassette('api_particulier/success/situation_pole_emploi') do
|
fill_in "Identifiant", with: identifiant
|
||||||
fill_in "Identifiant", with: identifiant
|
wait_until { pole_emploi_champ.reload.external_id.present? }
|
||||||
wait_until { pole_emploi_champ.reload.external_id.present? }
|
|
||||||
click_on 'Déposer le dossier'
|
click_on 'Déposer le dossier'
|
||||||
perform_enqueued_jobs
|
perform_enqueued_jobs
|
||||||
end
|
|
||||||
expect(page).to have_current_path(merci_dossier_path(Dossier.last))
|
expect(page).to have_current_path(merci_dossier_path(Dossier.last))
|
||||||
|
|
||||||
perform_enqueued_jobs
|
perform_enqueued_jobs
|
||||||
|
@ -406,7 +404,6 @@ describe 'fetch API Particulier Data', js: true, retry: 3 do
|
||||||
click_button('Continuer')
|
click_button('Continuer')
|
||||||
|
|
||||||
fill_in "INE", with: 'wrong code'
|
fill_in "INE", with: 'wrong code'
|
||||||
wait_for_autosave
|
|
||||||
|
|
||||||
dossier = Dossier.last
|
dossier = Dossier.last
|
||||||
mesri_champ = dossier.champs_public.find(&:mesri?)
|
mesri_champ = dossier.champs_public.find(&:mesri?)
|
||||||
|
@ -471,7 +468,6 @@ describe 'fetch API Particulier Data', js: true, retry: 3 do
|
||||||
|
|
||||||
fill_in 'Le numéro fiscal', with: numero_fiscal
|
fill_in 'Le numéro fiscal', with: numero_fiscal
|
||||||
fill_in "La référence d’avis d’imposition", with: 'wrong_code'
|
fill_in "La référence d’avis d’imposition", with: 'wrong_code'
|
||||||
wait_for_autosave
|
|
||||||
|
|
||||||
dossier = Dossier.last
|
dossier = Dossier.last
|
||||||
dgfip_champ = dossier.champs_public.find(&:dgfip?)
|
dgfip_champ = dossier.champs_public.find(&:dgfip?)
|
||||||
|
@ -482,7 +478,8 @@ describe 'fetch API Particulier Data', js: true, retry: 3 do
|
||||||
expect(page).to have_content(/dgfip doit posséder 13 ou 14 caractères/)
|
expect(page).to have_content(/dgfip doit posséder 13 ou 14 caractères/)
|
||||||
|
|
||||||
fill_in "La référence d’avis d’imposition", with: reference_avis
|
fill_in "La référence d’avis d’imposition", with: reference_avis
|
||||||
wait_for_autosave
|
wait_until { dgfip_champ.reload.external_id.present? }
|
||||||
|
|
||||||
click_on 'Déposer le dossier'
|
click_on 'Déposer le dossier'
|
||||||
perform_enqueued_jobs
|
perform_enqueued_jobs
|
||||||
|
|
||||||
|
|
|
@ -148,7 +148,9 @@ describe 'The user' do
|
||||||
create(:procedure, :published, :for_individual, types_de_champ_public: [
|
create(:procedure, :published, :for_individual, types_de_champ_public: [
|
||||||
{ mandatory: true, libelle: 'texte obligatoire' }, { mandatory: false, libelle: 'texte optionnel' },
|
{ mandatory: true, libelle: 'texte obligatoire' }, { mandatory: false, libelle: 'texte optionnel' },
|
||||||
{ mandatory: false, libelle: "nombre entier", type: :integer_number },
|
{ mandatory: false, libelle: "nombre entier", type: :integer_number },
|
||||||
{ mandatory: false, libelle: "nombre décimal", type: :decimal_number }
|
{ mandatory: false, libelle: "nombre décimal", type: :decimal_number },
|
||||||
|
{ mandatory: false, libelle: 'address', type: :address },
|
||||||
|
{ mandatory: false, libelle: 'IBAN', type: :iban }
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,6 +164,17 @@ describe 'The user' do
|
||||||
|
|
||||||
expect(page).to have_current_path(brouillon_dossier_path(user_dossier))
|
expect(page).to have_current_path(brouillon_dossier_path(user_dossier))
|
||||||
|
|
||||||
|
fill_in('IBAN', with: 'FR')
|
||||||
|
wait_until { champ_value_for('IBAN') == 'FR' }
|
||||||
|
|
||||||
|
expect(page).not_to have_content 'n’est pas au format IBAN'
|
||||||
|
blur
|
||||||
|
expect(page).to have_content 'n’est pas au format IBAN'
|
||||||
|
|
||||||
|
fill_in('IBAN', with: 'FR7630006000011234567890189')
|
||||||
|
wait_until { champ_value_for('IBAN') == 'FR76 3000 6000 0112 3456 7890 189' }
|
||||||
|
expect(page).not_to have_content 'n’est pas au format IBAN'
|
||||||
|
|
||||||
# Check an incomplete dossier cannot be submitted when mandatory fields are missing
|
# Check an incomplete dossier cannot be submitted when mandatory fields are missing
|
||||||
click_on 'Déposer le dossier'
|
click_on 'Déposer le dossier'
|
||||||
expect(user_dossier.reload.brouillon?).to be(true)
|
expect(user_dossier.reload.brouillon?).to be(true)
|
||||||
|
@ -169,15 +182,15 @@ describe 'The user' do
|
||||||
|
|
||||||
# Check a dossier can be submitted when all mandatory fields are filled
|
# Check a dossier can be submitted when all mandatory fields are filled
|
||||||
fill_in('texte obligatoire', with: 'super texte')
|
fill_in('texte obligatoire', with: 'super texte')
|
||||||
|
wait_until { champ_value_for('texte obligatoire') == 'super texte' }
|
||||||
|
|
||||||
click_on 'Déposer le dossier'
|
click_on 'Déposer le dossier'
|
||||||
wait_until { user_dossier.reload.en_construction? }
|
wait_until { user_dossier.reload.en_construction? }
|
||||||
expect(champ_value_for('texte obligatoire')).to eq('super texte')
|
|
||||||
expect(page).to have_current_path(merci_dossier_path(user_dossier))
|
expect(page).to have_current_path(merci_dossier_path(user_dossier))
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario 'fill address not in BAN', js: true, retry: 3 do
|
scenario 'fill address not in BAN', js: true, retry: 3 do
|
||||||
log_in(user, procedure)
|
log_in(user, simple_procedure)
|
||||||
fill_individual
|
fill_individual
|
||||||
|
|
||||||
fill_in('address', with: '2 rue de la paix, 92094 Belgique')
|
fill_in('address', with: '2 rue de la paix, 92094 Belgique')
|
||||||
|
|
|
@ -12,6 +12,7 @@ RSpec.shared_examples 'the user can edit the submitted demande' do
|
||||||
fill_in('Texte obligatoire', with: 'Nouveau texte')
|
fill_in('Texte obligatoire', with: 'Nouveau texte')
|
||||||
|
|
||||||
click_on 'Déposer les modifications'
|
click_on 'Déposer les modifications'
|
||||||
|
expect(page).to have_current_path(dossier_path(dossier))
|
||||||
click_on 'Demande'
|
click_on 'Demande'
|
||||||
expect(page).to have_current_path(demande_dossier_path(dossier))
|
expect(page).to have_current_path(demande_dossier_path(dossier))
|
||||||
|
|
||||||
|
|
|
@ -1218,6 +1218,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@coldwired/utils/-/utils-0.11.1.tgz#d126246ab66591467e9e4c765769f133f6236ddb"
|
resolved "https://registry.yarnpkg.com/@coldwired/utils/-/utils-0.11.1.tgz#d126246ab66591467e9e4c765769f133f6236ddb"
|
||||||
integrity sha512-vuW1hVhD5U4NX/0dl+fT4RL92T5fEITwc9l/DnaBoP5SAuCAVRnHCWcdyROGz55E4WvSXFqAHD6qkxRwOKG2og==
|
integrity sha512-vuW1hVhD5U4NX/0dl+fT4RL92T5fEITwc9l/DnaBoP5SAuCAVRnHCWcdyROGz55E4WvSXFqAHD6qkxRwOKG2og==
|
||||||
|
|
||||||
|
"@coldwired/utils@^0.11.4":
|
||||||
|
version "0.11.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@coldwired/utils/-/utils-0.11.4.tgz#4889103dc73c1d86eff7d21574a0bde37b1f3c03"
|
||||||
|
integrity sha512-JvYosEc++zcuZmyfHAKG1cZ/jEIBM8ciJsD2ZcgFdzjSla1wtG+p6GOYiIVXZpCKO2oWGR4EmiYFYpBNIy3WDw==
|
||||||
|
|
||||||
"@emotion/is-prop-valid@^0.8.2":
|
"@emotion/is-prop-valid@^0.8.2":
|
||||||
version "0.8.8"
|
version "0.8.8"
|
||||||
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
|
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
|
||||||
|
|
Loading…
Reference in a new issue