diff --git a/package.json b/package.json index 64004f37..da3b8435 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "salix": "cd ../salix && gulp back", "db": "cd ../salix && gulp docker", "cy:open": "npm run db && cypress open", - "test:e2e": "npm run db && cypress run", + "test:e2e": "npm run db && cypress run --headed --config video=false", "test:unit": "vitest", "build": "rm -rf dist/ ; quasar build", "clean": "rm -rf dist/", diff --git a/quasar.config.js b/quasar.config.js index 84e934d1..a9d50c62 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -23,7 +23,7 @@ module.exports = configure(function (ctx) { // app boot file (/src/boot) // --> boot files are part of "main.js" // https://v2.quasar.dev/quasar-cli-webpack/boot-files - boot: ['i18n', 'axios', 'vnDate', 'error-handler', 'app'], + boot: ['i18n', 'axios', 'vnDate', 'app'], // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css css: ['app.scss', 'width.scss', 'responsive.scss'], diff --git a/src/boot/error-handler.js b/src/boot/error-handler.js deleted file mode 100644 index 1e11c952..00000000 --- a/src/boot/error-handler.js +++ /dev/null @@ -1,66 +0,0 @@ -export default async ({ app }) => { - /* - window.addEventListener('error', - e => onWindowError(e)); - window.addEventListener('unhandledrejection', - e => onWindowRejection(e)); - - ,onWindowError(event) { - errorHandler(event.error); - } - ,onWindowRejection(event) { - errorHandler(event.reason); - } -*/ - app.config.errorHandler = (err, vm, info) => { - errorHandler(err, vm) - } - - function errorHandler (err, vm) { - let message - let tMessage - let res = err.response - - // XXX: Compatibility with old JSON service - if (err.name === 'JsonException') { - res = { - status: err.statusCode, - data: { error: { message: err.message } } - } - } - - if (res) { - const status = res.status - - if (status >= 400 && status < 500) { - switch (status) { - case 401: - tMessage = 'loginFailed' - break - case 403: - tMessage = 'authenticationRequired' - vm.$router.push('/login') - break - case 404: - tMessage = 'notFound' - break - default: - message = res.data.error.message - } - } else if (status >= 500) { - tMessage = 'internalServerError' - } - } else { - tMessage = 'somethingWentWrong' - console.error(err) - } - - if (tMessage) { - message = vm.$t(tMessage) - } - vm.$q.notify({ - message, - type: 'negative' - }) - } -} diff --git a/src/components/common/FormModel.vue b/src/components/common/FormModel.vue new file mode 100644 index 00000000..fc5d1217 --- /dev/null +++ b/src/components/common/FormModel.vue @@ -0,0 +1,231 @@ +<script setup> +import { ref, inject, onMounted, computed, Teleport, watch } from 'vue'; +import { useI18n } from 'vue-i18n'; + +import { useAppStore } from 'stores/app'; +import { storeToRefs } from 'pinia'; +import useNotify from 'src/composables/useNotify.js'; + +const props = defineProps({ + title: { + type: String, + default: '' + }, + table: { + type: String, + default: '' + }, + schema: { + type: String, + default: '' + }, + // Objeto con los datos iniciales del form, si este objeto es definido, no se ejecuta la query fetch + formInitialData: { + type: Object, + default: () => {} + }, + autoLoad: { + type: Boolean, + default: true + }, + defaultActions: { + type: Boolean, + default: true + }, + showBottomActions: { + type: Boolean, + default: false + }, + saveFn: { + type: Function, + default: null + }, + separationBetweenInputs: { + type: String, + default: 'xs' + }, + url: { + type: String, + default: '' + }, + urlUpdate: { + type: String, + default: null + }, + urlCreate: { + type: String, + default: null + }, + observeFormChanges: { + type: Boolean, + default: true, + description: + 'Esto se usa principalmente para permitir guardar sin hacer cambios (Útil para la feature de clonar ya que en este caso queremos poder guardar de primeras)' + }, + mapper: { + type: Function, + default: null + }, + filter: { + type: Object, + default: null + } +}); + +const emit = defineEmits(['onDataSaved', 'onDataFetched']); +const api = inject('api'); +const { t } = useI18n(); +const { notify } = useNotify(); +const appStore = useAppStore(); +const { isHeaderMounted } = storeToRefs(appStore); + +const isLoading = ref(false); +const formData = ref({}); +const formModelRef = ref(null); +const hasChanges = ref(!props.observeFormChanges); +const isResetting = ref(false); +const originalData = ref(null); + +const separationBetweenInputs = computed(() => { + return `q-gutter-y-${props.separationBetweenInputs}`; +}); + +const onSubmitSuccess = () => { + emit('onDataSaved'); + notify(t('dataSaved'), 'positive'); +}; + +onMounted(async () => { + if (!props.formInitialData) { + if (props.autoLoad && props.url) await fetch(); + originalData.value = { ...formData.value }; + } else { + formData.value = { ...props.formInitialData }; + originalData.value = { ...props.formInitialData }; + } + + if (props.observeFormChanges) { + watch( + () => formData.value, + (newVal, oldVal) => { + if (!oldVal) return; + hasChanges.value = + !isResetting.value && + JSON.stringify(newVal) !== + JSON.stringify(originalData.value); + isResetting.value = false; + }, + { deep: true } + ); + } +}); + +async function fetch() { + try { + let { data } = await api.get(props.url, { + params: { filter: JSON.stringify(props.filter) } + }); + if (Array.isArray(data)) data = data[0] ?? {}; + formData.value = { ...data }; + emit('onDataFetched', formData.value); + } catch (e) { + throw e; + } +} + +const submitForm = async evt => { + const isFormValid = await formModelRef.value.validate(); + if (isFormValid) await save(evt); +}; + +async function save() { + if (props.observeFormChanges && !hasChanges.value) + return notify('noChanges', 'negative'); + + isLoading.value = true; + try { + const body = props.mapper + ? props.mapper(formData.value, originalData.value) + : formData.value; + const method = props.urlCreate ? 'post' : 'patch'; + const url = props.urlCreate || props.urlUpdate || props.url; + + await Promise.resolve( + props.saveFn ? props.saveFn(body) : api[method](url, body) + ); + + onSubmitSuccess(); + hasChanges.value = false; + } finally { + isLoading.value = false; + } +} + +defineExpose({ + formData, + submitForm +}); +</script> + +<template> + <QCard class="form-container" v-bind="$attrs"> + <QForm ref="formModelRef" class="form" :class="separationBetweenInputs"> + <span v-if="title" class="text-h6 text-bold"> + {{ title }} + </span> + <slot name="form" :data="formData" /> + <slot name="extraForm" :data="formData" /> + <component + v-if="isHeaderMounted" + :is="showBottomActions ? 'div' : Teleport" + to="#actions" + class="flex row justify-end q-gutter-x-sm" + :class="{ 'q-mt-md': showBottomActions }" + > + <QBtn + v-if="defaultActions && showBottomActions" + :label="t('cancel')" + :icon="showBottomActions ? undefined : 'check'" + rounded + no-caps + flat + v-close-popup + > + <QTooltip>{{ t('cancel') }}</QTooltip> + </QBtn> + <QBtn + v-if="defaultActions" + :label="t('save')" + :icon="showBottomActions ? undefined : 'check'" + rounded + no-caps + flat + :loading="isLoading" + :disabled="!showBottomActions && !hasChanges" + @click="submitForm()" + data-cy="formModelDefaultSaveButton" + > + <QTooltip>{{ t('save') }}</QTooltip> + </QBtn> + <slot name="actions" :data="formData" /> + </component> + </QForm> + </QCard> +</template> + +<style lang="scss" scoped> +.form-container { + width: 100%; + height: max-content; + padding: 32px; + max-width: 544px; + display: flex; + justify-content: center; +} + +.form { + display: flex; + flex-direction: column; + width: 100%; +} +</style> diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue index 508ce132..f20754ee 100644 --- a/src/components/common/VnInput.vue +++ b/src/components/common/VnInput.vue @@ -1,15 +1,20 @@ <script setup> -import { computed, ref } from 'vue'; +import { computed, ref, useAttrs, nextTick } from 'vue'; import { useI18n } from 'vue-i18n'; +import { useRequired } from 'src/composables/useRequired'; +const $attrs = useAttrs(); +const { isRequired, requiredFieldRule } = useRequired($attrs); +const { t } = useI18n(); const emit = defineEmits([ 'update:modelValue', 'update:options', 'keyup.enter', - 'remove' + 'remove', + 'blur' ]); -const props = defineProps({ +const $props = defineProps({ modelValue: { type: [String, Number], default: null @@ -25,24 +30,43 @@ const props = defineProps({ clearable: { type: Boolean, default: true + }, + emptyToNull: { + type: Boolean, + default: true + }, + insertable: { + type: Boolean, + default: false + }, + maxlength: { + type: Number, + default: null + }, + uppercase: { + type: Boolean, + default: false } }); -const { t } = useI18n(); -const requiredFieldRule = val => !!val || t('globals.fieldRequired'); const vnInputRef = ref(null); const value = computed({ get() { - return props.modelValue; + return $props.modelValue; }, set(value) { + if ($props.emptyToNull && value === '') value = null; emit('update:modelValue', value); } }); const hover = ref(false); const styleAttrs = computed(() => { - return props.isOutlined - ? { dense: true, outlined: true, rounded: true } + return $props.isOutlined + ? { + dense: true, + outlined: true, + rounded: true + } : {}; }); @@ -51,66 +75,119 @@ const focus = () => { }; defineExpose({ - focus + focus, + vnInputRef }); -const inputRules = [ +const mixinRules = [ + requiredFieldRule, + ...($attrs.rules ?? []), val => { + const { maxlength } = vnInputRef.value; + if (maxlength && +val.length > maxlength) + return t(`maxLength`, { value: maxlength }); const { min, max } = vnInputRef.value.$attrs; - if (min >= 0) { + if (!min) return null; + if (min >= 0) if (Math.floor(val) < min) return t('inputMin', { value: min }); - } + if (!max) return null; if (max > 0) { if (Math.floor(val) > max) return t('inputMax', { value: max }); } } ]; + +const handleKeydown = e => { + if (e.key === 'Backspace') return; + + if ($props.insertable && e.key.match(/[0-9]/)) { + handleInsertMode(e); + } +}; + +const handleInsertMode = e => { + e.preventDefault(); + const input = e.target; + const cursorPos = input.selectionStart; + const { maxlength } = vnInputRef.value; + let currentValue = value.value; + if (!currentValue) currentValue = e.key; + const newValue = e.key; + if (newValue && !isNaN(newValue) && cursorPos < maxlength) { + value.value = + currentValue.substring(0, cursorPos) + + newValue + + currentValue.substring(cursorPos + 1); + } + nextTick(() => { + input.setSelectionRange(cursorPos + 1, cursorPos + 1); + }); +}; + +const handleUppercase = () => { + value.value = value.value?.toUpperCase() || ''; +}; </script> <template> - <div - :rules="$attrs.required ? [requiredFieldRule] : null" - @mouseover="hover = true" - @mouseleave="hover = false" - > + <div @mouseover="hover = true" @mouseleave="hover = false"> <QInput ref="vnInputRef" v-model="value" v-bind="{ ...$attrs, ...styleAttrs }" :type="$attrs.type" - :class="{ required: $attrs.required }" + :class="{ required: isRequired }" + @keyup.enter="emit('keyup.enter')" + @blur="emit('blur')" + @keydown="handleKeydown" :clearable="false" - :rules="inputRules" + :rules="mixinRules" :lazy-rules="true" hide-bottom-space - @keyup.enter="emit('keyup.enter')" + :data-cy="$attrs['data-cy'] ?? $attrs.label + '_input'" > - <template - v-if="$slots.prepend" - #prepend - > + <template #prepend v-if="$slots.prepend"> <slot name="prepend" /> </template> <template #append> - <slot - v-if="$slots.append && !$attrs.disabled" - name="append" - /> <QIcon - v-if="hover && value && !$attrs.disabled && props.clearable" name="close" size="xs" + :style="{ + visibility: + hover && + value && + !$attrs.disabled && + !$attrs.readonly && + $props.clearable + ? 'visible' + : 'hidden' + }" @click=" () => { value = null; + vnInputRef.focus(); emit('remove'); } " - /> + ></QIcon> + <QIcon - v-if="info" - name="info" + name="match_case" + size="xs" + v-if=" + !$attrs.disabled && !$attrs.readonly && $props.uppercase + " + @click="handleUppercase" + class="uppercase-icon" > + <QTooltip> + {{ t('Convert to uppercase') }} + </QTooltip> + </QIcon> + + <slot name="append" v-if="$slots.append && !$attrs.disabled" /> + <QIcon v-if="info" name="info"> <QTooltip max-width="350px"> {{ info }} </QTooltip> @@ -120,20 +197,44 @@ const inputRules = [ </div> </template> +<style> +.uppercase-icon { + transition: + color 0.3s, + transform 0.2s; + cursor: pointer; +} + +.uppercase-icon:hover { + color: #ed9937; + transform: scale(1.2); +} +</style> + <i18n lang="yaml"> en-US: inputMin: Must be more than {value} + maxLength: The value exceeds {value} characters inputMax: Must be less than {value} + Convert to uppercase: Convert to uppercase es-ES: inputMin: Debe ser mayor a {value} + maxLength: El valor excede los {value} carácteres inputMax: Debe ser menor a {value} + Convert to uppercase: Convertir a mayúsculas ca-ES: inputMin: Ha de ser més gran que {value} - inputMax: Ha de ser menys que {value} + maxLength: El valor excedeix els {value} caràcters + inputMax: Ha de ser menor que {value} + Convert to uppercase: Convertir a majúscules fr-FR: inputMin: Doit être supérieur à {value} - inputMax: Doit être supérieur à {value} + maxLength: La valeur dépasse {value} caractères + inputMax: Doit être inférieur à {value} + Convert to uppercase: Convertir en majuscules pt-PT: inputMin: Deve ser maior que {value} - inputMax: Deve ser maior que {value} + maxLength: O valor excede {value} caracteres + inputMax: Deve ser menor que {value} + Convert to uppercase: Converter para maiúsculas </i18n> diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index c4a3e1df..65dbb2f2 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -67,7 +67,7 @@ const $props = defineProps({ }); const { t } = useI18n(); -const requiredFieldRule = val => val ?? t('globals.fieldRequired'); +const requiredFieldRule = val => val ?? t('fieldRequired'); const { optionLabel, optionValue, options } = toRefs($props); const myOptions = ref([]); diff --git a/src/composables/useRequired.js b/src/composables/useRequired.js new file mode 100644 index 00000000..a77345b5 --- /dev/null +++ b/src/composables/useRequired.js @@ -0,0 +1,17 @@ +import { useI18n } from 'vue-i18n'; + +export function useRequired($attrs) { + const { t } = useI18n(); + + const isRequired = + typeof $attrs['required'] === 'boolean' + ? $attrs['required'] + : Object.keys($attrs).includes('required'); + const requiredFieldRule = val => + isRequired ? !!val || t('fieldRequired') : null; + + return { + isRequired, + requiredFieldRule + }; +} diff --git a/src/i18n/ca-ES/index.js b/src/i18n/ca-ES/index.js index 456795a5..48d214e0 100644 --- a/src/i18n/ca-ES/index.js +++ b/src/i18n/ca-ES/index.js @@ -134,6 +134,8 @@ export default { introduceSearchTerm: 'Introdueix un terme de cerca', noOrdersFound: `No s'han trobat comandes`, send: 'Enviar', + fieldRequired: 'Aquest camp és obligatori', + noChanges: 'No s’han fet canvis', // Image related translations 'Cant lock cache': 'No es pot bloquejar la memòria cau', 'Bad file format': 'Format de fitxer no reconegut', diff --git a/src/i18n/en-US/index.js b/src/i18n/en-US/index.js index 3e21b66d..80107dd5 100644 --- a/src/i18n/en-US/index.js +++ b/src/i18n/en-US/index.js @@ -167,6 +167,8 @@ export default { introduceSearchTerm: 'Enter a search term', noOrdersFound: 'No orders found', send: 'Send', + fieldRequired: 'Field required', + noChanges: 'No changes', // Image related translations 'Cant lock cache': 'The cache could not be blocked', 'Bad file format': 'Unrecognized file format', diff --git a/src/i18n/es-ES/index.js b/src/i18n/es-ES/index.js index 5034ff70..15639c2d 100644 --- a/src/i18n/es-ES/index.js +++ b/src/i18n/es-ES/index.js @@ -166,6 +166,8 @@ export default { introduceSearchTerm: 'Introduce un término de búsqueda', noOrdersFound: 'No se encontrado pedidos', send: 'Enviar', + fieldRequired: 'Campo requerido', + noChanges: 'No se han hecho cambios', // Image related translations 'Cant lock cache': 'La caché no pudo ser bloqueada', 'Bad file format': 'Formato de archivo no reconocido', diff --git a/src/i18n/fr-FR/index.js b/src/i18n/fr-FR/index.js index d30dee52..2eea9a38 100644 --- a/src/i18n/fr-FR/index.js +++ b/src/i18n/fr-FR/index.js @@ -134,6 +134,8 @@ export default { introduceSearchTerm: 'Entrez un terme de recherche', noOrdersFound: 'Aucune commande trouvée', send: 'Envoyer', + fieldRequired: 'Champ obligatoire', + noChanges: 'Aucun changement', // Image related translations 'Cant lock cache': "Le cache n'a pas pu être verrouillé", 'Bad file format': 'Format de fichier non reconnu', diff --git a/src/i18n/pt-PT/index.js b/src/i18n/pt-PT/index.js index 39bbe5de..460335e0 100644 --- a/src/i18n/pt-PT/index.js +++ b/src/i18n/pt-PT/index.js @@ -133,6 +133,8 @@ export default { introduceSearchTerm: 'Digite um termo de pesquisa', noOrdersFound: 'Nenhum pedido encontrado', send: 'Enviar', + fieldRequired: 'Campo obrigatório', + noChanges: 'Sem alterações', // Image related translations 'Cant lock cache': 'O cache não pôde ser bloqueado', 'Bad file format': 'Formato de arquivo inválido', diff --git a/src/pages/Account/AddressDetails.vue b/src/pages/Account/AddressDetails.vue index 9e4210d7..d60684ad 100644 --- a/src/pages/Account/AddressDetails.vue +++ b/src/pages/Account/AddressDetails.vue @@ -5,32 +5,26 @@ import { useRouter, useRoute } from 'vue-router'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; -import VnForm from 'src/components/common/VnForm.vue'; +import FormModel from 'src/components/common/FormModel.vue'; +import { useUserStore } from 'stores/user'; import { useAppStore } from 'stores/app'; import { storeToRefs } from 'pinia'; const router = useRouter(); const route = useRoute(); const { t } = useI18n(); -const jApi = inject('jApi'); +const api = inject('api'); const appStore = useAppStore(); +const userStore = useUserStore(); const { isHeaderMounted } = storeToRefs(appStore); const vnFormRef = ref(null); const countriesOptions = ref([]); const provincesOptions = ref([]); -const pks = { id: route.params.id }; const isEditMode = route.params.id !== '0'; -const fetchAddressDataSql = { - query: ` - SELECT a.id, a.street, a.nickname, a.city, a.postalCode, a.provinceFk, p.countryFk - FROM myAddress a - LEFT JOIN vn.province p ON p.id = a.provinceFk - WHERE a.id = #address - `, - params: { address: route.params.id } -}; +const editAddressData = ref(null); +const showForm = ref(false); watch( () => vnFormRef?.value?.formData?.countryFk, @@ -40,23 +34,49 @@ watch( const goBack = () => router.push({ name: 'addressesList' }); const getCountries = async () => { - countriesOptions.value = await jApi.query( - `SELECT id, name FROM vn.country - ORDER BY name` - ); + const filter = { fields: ['id', 'name'], order: 'name' }; + const { data } = await api.get('Countries', { + params: { filter: JSON.stringify(filter) } + }); + countriesOptions.value = data; }; const getProvinces = async countryFk => { if (!countryFk) return; - provincesOptions.value = await jApi.query( - `SELECT id, name FROM vn.province - WHERE countryFk = #id - ORDER BY name`, - { id: countryFk } - ); + + const filter = { + where: { countryFk }, + fields: ['id', 'name'], + order: 'name' + }; + const { data } = await api.get('Provinces', { + params: { filter: JSON.stringify(filter) } + }); + provincesOptions.value = data; }; -onMounted(() => getCountries()); +const getAddressDetails = async () => { + const { data } = await api.get(`Addresses/${route.params.id}`); + if (!data) return; + + const { nickname, street, city, postalCode, province, provinceFk } = data; + editAddressData.value = { + nickname, + street, + city, + postalCode, + countryFk: province?.countryFk, + provinceFk + }; +}; + +onMounted(async () => { + if (isEditMode) { + await getAddressDetails(); + } + getCountries(); + showForm.value = true; +}); </script> <template> @@ -74,42 +94,49 @@ onMounted(() => getCountries()); </QTooltip> </QBtn> </Teleport> - <VnForm + <FormModel + v-if="showForm" ref="vnFormRef" - :fetch-form-data-sql="fetchAddressDataSql" - :columns-to-ignore-update="['countryFk']" - :create-model-default="{ - field: 'clientFk', - value: 'account.myUser_getId()' - }" - :pks="pks" - :is-edit-mode="isEditMode" + :url-create=" + !isEditMode + ? `Clients/${userStore?.user?.id}/createAddress` + : '' + " + :url-update=" + isEditMode + ? `Clients/${userStore?.user?.id}/updateAddress/${route?.params?.id}` + : '' + " + :form-initial-data="editAddressData" :title="t(isEditMode ? 'editAddress' : 'addAddress')" - table="myAddress" - schema="hedera" @on-data-saved="goBack()" + :show-bottom-actions="false" > <template #form="{ data }"> <VnInput v-model="data.nickname" :label="t('name')" data-cy="addressFormNickname" + required /> <VnInput v-model="data.street" :label="t('address')" data-cy="addressFormStreet" + required /> <VnInput v-model="data.city" :label="t('city')" data-cy="addressFormCity" + required /> <VnInput v-model="data.postalCode" type="number" :label="t('postalCode')" data-cy="addressFormPostcode" + required /> <VnSelect v-model="data.countryFk" @@ -117,15 +144,17 @@ onMounted(() => getCountries()); :options="countriesOptions" @update:model-value="data.provinceFk = null" data-cy="addressFormCountry" + required /> <VnSelect v-model="data.provinceFk" :label="t('province')" :options="provincesOptions" data-cy="addressFormProvince" + required /> </template> - </VnForm> + </FormModel> </QPage> </template> diff --git a/src/pages/Account/AddressList.vue b/src/pages/Account/AddressList.vue index 4a8305e8..9939c926 100644 --- a/src/pages/Account/AddressList.vue +++ b/src/pages/Account/AddressList.vue @@ -15,6 +15,7 @@ import { useUserStore } from 'stores/user'; const router = useRouter(); const jApi = inject('jApi'); +const api = inject('api'); const { notify } = useNotify(); const { t } = useI18n(); const { openConfirmationModal } = useVnConfirm(); @@ -56,16 +57,13 @@ const changeDefaultAddress = async () => { notify(t('defaultAddressModified'), 'positive'); }; -const removeAddress = async id => { +async function removeAddress(address) { try { - await jApi.execQuery( - `START TRANSACTION; - UPDATE hedera.myAddress SET isActive = FALSE - WHERE ((id = #id)); - SELECT isActive FROM hedera.myAddress WHERE ((id = #id)); - COMMIT`, + await api.patch( + `/Clients/${userStore?.user?.id}/updateAddress/${address.id}`, { - id + ...address, + isActive: false } ); fetchAddressesRef.value.fetch(); @@ -73,7 +71,7 @@ const removeAddress = async id => { } catch (error) { console.error('Error removing address:', error); } -}; +} onMounted(async () => { getDefaultAddress(); @@ -153,7 +151,7 @@ onMounted(async () => { openConfirmationModal( null, t('confirmDeleteAddress'), - () => removeAddress(address.id) + () => removeAddress(address) ) " > diff --git a/src/test/cypress/integration/config/AddresList.spec.js b/src/test/cypress/integration/config/AddresList.spec.js index d8a39271..6b5ad682 100644 --- a/src/test/cypress/integration/config/AddresList.spec.js +++ b/src/test/cypress/integration/config/AddresList.spec.js @@ -46,15 +46,28 @@ describe('PendingOrders', () => { .should('contain', data.postcode); }; + it('should fail if we enter a wrong postcode', () => { + cy.dataCy('newAddressBtn').should('exist'); + cy.dataCy('newAddressBtn').click(); + cy.dataCy('formModelDefaultSaveButton').should('exist'); + cy.dataCy('formModelDefaultSaveButton').should('be.disabled'); + const addressFormData = getRandomAddressFormData(); + fillFormWithData(addressFormData); + cy.dataCy('formModelDefaultSaveButton').should('not.be.disabled'); + cy.dataCy('formModelDefaultSaveButton').click(); + cy.checkNotify('negative'); + }); + it('should create a new address', () => { cy.dataCy('newAddressBtn').should('exist'); cy.dataCy('newAddressBtn').click(); - cy.dataCy('formDefaultSaveButton').should('exist'); - cy.dataCy('formDefaultSaveButton').should('be.disabled'); + cy.dataCy('formModelDefaultSaveButton').should('exist'); + cy.dataCy('formModelDefaultSaveButton').should('be.disabled'); const addressFormData = getRandomAddressFormData(); + addressFormData.postcode = '46460'; // Usamos un postcode válido fillFormWithData(addressFormData); - cy.dataCy('formDefaultSaveButton').should('not.be.disabled'); - cy.dataCy('formDefaultSaveButton').click(); + cy.dataCy('formModelDefaultSaveButton').should('not.be.disabled'); + cy.dataCy('formModelDefaultSaveButton').click(); cy.checkNotify('positive', 'Datos guardados'); verifyAddressCardData(addressFormData); }); @@ -71,9 +84,10 @@ describe('PendingOrders', () => { }); // Fill form with new data const addressFormData = getRandomAddressFormData(); + addressFormData.postcode = '46460'; // Usamos un postcode válido fillFormWithData(addressFormData); - cy.dataCy('formDefaultSaveButton').should('not.be.disabled'); - cy.dataCy('formDefaultSaveButton').click(); + cy.dataCy('formModelDefaultSaveButton').should('not.be.disabled'); + cy.dataCy('formModelDefaultSaveButton').click(); cy.checkNotify('positive', 'Datos guardados'); verifyAddressCardData(addressFormData); }); diff --git a/src/test/cypress/integration/UserFlows.spec.js b/src/test/cypress/integration/flows/UserFlows.spec.js similarity index 100% rename from src/test/cypress/integration/UserFlows.spec.js rename to src/test/cypress/integration/flows/UserFlows.spec.js diff --git a/src/test/cypress/support/commands.js b/src/test/cypress/support/commands.js index 2d4de848..cb22f86a 100644 --- a/src/test/cypress/support/commands.js +++ b/src/test/cypress/support/commands.js @@ -90,5 +90,6 @@ Cypress.Commands.add('setConfirmDialog', () => { }); Cypress.Commands.add('checkNotify', (status, content) => { - cy.dataCy(`${status}Notify`).should('contain', content); + if (content) cy.dataCy(`${status}Notify`).should('contain', content); + else cy.dataCy(`${status}Notify`).should('exist'); });