ref #5835 e2e & unit tests done
gitea/salix-front/pipeline/head There was a failure building this commit Details

This commit is contained in:
Jorge Penadés 2023-11-09 10:29:28 +01:00
parent a9241b17f5
commit c31b4fa770
28 changed files with 443 additions and 84 deletions

View File

@ -8,7 +8,7 @@ module.exports = defineConfig({
supportFile: 'test/cypress/support/index.js',
videosFolder: 'test/cypress/videos',
video: false,
specPattern: 'test/cypress/integration/*.spec.js',
specPattern: 'test/cypress/integration/**/*.spec.js',
experimentalRunAllSpecs: true,
component: {
componentFolder: 'src',

View File

@ -135,6 +135,10 @@ async function saveChanges(data) {
hasChanges.value = false;
isLoading.value = false;
emit('saveChanges', data);
quasar.notify({
type: 'positive',
message: t('globals.dataSaved'),
});
}
async function insert() {

View File

@ -33,6 +33,7 @@ body.body--light {
--vn-gray: #f5f5f5;
--vn-label: #5f5f5f;
--vn-dark: white;
--vn-light-gray: #e7e3e3;
}
body.body--dark {
@ -40,6 +41,7 @@ body.body--dark {
--vn-gray: #313131;
--vn-label: #a8a8a8;
--vn-dark: #292929;
--vn-light-gray: #424242;
}
.bg-vn-dark {

View File

@ -9,13 +9,10 @@ export default function (value, symbol = 'EUR', fractionSize = 2) {
style: 'currency',
currency: symbol,
minimumFractionDigits: fractionSize,
maximumFractionDigits: fractionSize
maximumFractionDigits: fractionSize,
};
const lang = locale.value == 'es' ? 'de' : locale.value;
return new Intl.NumberFormat(lang, options)
.format(value);
}
return new Intl.NumberFormat(lang, options).format(value);
}

View File

@ -422,7 +422,7 @@ export default {
company: 'Empresa',
customerCard: 'Ficha del cliente',
ticketList: 'Listado de tickets',
vat: 'Vat',
vat: 'Iva',
dueDay: 'Fecha de vencimiento',
},
summary: {

View File

@ -144,7 +144,6 @@ async function create() {
type: 'positive',
});
} catch (error) {
console.log(error);
quasar.notify({
message: t(`${error.message}`),
type: 'negative',
@ -466,6 +465,8 @@ async function create() {
class="full-width q-pa-xs"
:label="t('Reference')"
v-model="dms.reference"
clearable
clear-icon="close"
/>
<VnSelectFilter
class="full-width q-pa-xs"

View File

@ -2,6 +2,7 @@
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { toDate } from 'src/filters';
import { useArrayData } from 'src/composables/useArrayData';
import CrudModel from 'src/components/CrudModel.vue';
@ -16,15 +17,14 @@ const invoiceIn = computed(() => arrayData.store.data);
const rowsSelected = ref([]);
const banks = ref([]);
const invoiceInFormRef = ref();
const invoiceId = route.params.id;
const filter = {
where: {
invoiceInFk: route.params.id,
invoiceInFk: invoiceId,
},
};
const isNotEuro = (code) => code != 'EUR';
const columns = computed(() => [
{
name: 'duedate',
@ -63,6 +63,19 @@ const columns = computed(() => [
align: 'left',
},
]);
const isNotEuro = (code) => code != 'EUR';
const areRows = ref(null);
async function insert() {
if (!areRows.value) {
await axios.post('/InvoiceInDueDays/new ', { id: Number(invoiceId) });
await invoiceInFormRef.value.reload();
}
if (areRows.value) {
invoiceInFormRef.value.insert();
}
}
</script>
<template>
<FetchData url="Banks" auto-load limit="30" @on-fetch="(data) => (banks = data)" />
@ -73,8 +86,9 @@ const columns = computed(() => [
url="InvoiceInDueDays"
:filter="filter"
auto-load
:data-required="{ invoiceInFk: route.params.id }"
:data-required="{ invoiceInFk: invoiceId }"
v-model:selected="rowsSelected"
@on-fetch="(data) => (areRows = !!data.length)"
>
<template #body="{ rows }">
<QTable
@ -271,13 +285,7 @@ const columns = computed(() => [
</template>
</CrudModel>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
color="primary"
icon="add"
size="lg"
round
@click="invoiceInFormRef.insert()"
/>
<QBtn color="primary" icon="add" size="lg" round @click="insert" />
</QPageSticky>
</template>
<style lang="scss" scoped></style>

View File

@ -29,6 +29,7 @@ const entityId = computed(() => $props.id || route.params.id);
const salixUrl = ref();
const invoiceInUrl = ref();
const amountsNotMatch = ref(null);
const intrastatTotals = ref({});
const vatColumns = ref([
{
@ -74,7 +75,7 @@ const vatColumns = ref([
name: 'currency',
label: 'invoiceIn.summary.currency',
field: (row) => row.foreignValue,
format: (value) => toCurrency(value),
format: (value) => value,
sortable: true,
align: 'left',
},
@ -138,7 +139,7 @@ const intrastatColumns = ref([
align: 'left',
},
{
name: 'amount',
name: 'stems',
label: 'invoiceIn.summary.stems',
field: (row) => row.stems,
format: (value) => value,
@ -155,14 +156,29 @@ const intrastatColumns = ref([
},
]);
function setAmountNotMatch(entity) {
function getAmountNotMatch(totals) {
return (
totals.totalDueDay != totals.totalTaxableBase &&
totals.totalDueDay != totals.totalVat
);
}
function getIntrastatTotals(intrastat) {
const totals = {};
totals.amount = intrastat.reduce((acc, cur) => acc + cur.amount, 0);
totals.net = intrastat.reduce((acc, cur) => acc + cur.net, 0);
totals.stems = intrastat.reduce((acc, cur) => acc + cur.stems, 0);
return totals;
}
function setData(entity) {
if (!entity) return false;
const total = entity.totals;
amountsNotMatch.value = getAmountNotMatch(entity.totals);
amountsNotMatch.value =
total.totalDueDay != total.totalTaxableBase &&
total.totalDueDay != total.totalVat;
if (entity.invoiceInIntrastat.length)
intrastatTotals.value = { ...getIntrastatTotals(entity.invoiceInIntrastat) };
}
</script>
@ -170,12 +186,13 @@ function setAmountNotMatch(entity) {
<CardSummary
ref="summary"
:url="`InvoiceIns/${entityId}/summary`"
@on-fetch="(data) => setAmountNotMatch(data)"
@on-fetch="(data) => setData(data)"
>
<template #header="{ entity: invoiceIn }">
<div>{{ invoiceIn.id }} - {{ invoiceIn.supplier.name }}</div>
</template>
<template #body="{ entity: invoiceIn }">
<!--Basic Data-->
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<a class="header" :href="`#/invoice-in/${entityId}/basic-data`">
@ -271,7 +288,7 @@ function setAmountNotMatch(entity) {
</QBtn>
</QCardSection>
<QCardSection class="q-pa-none">
<div class="bordered q-px-sm">
<div class="bordered q-px-sm q-mx-auto">
<VnLv
:label="t('invoiceIn.summary.taxableBase')"
:value="toCurrency(invoiceIn.totals.totalTaxableBase)"
@ -336,6 +353,7 @@ function setAmountNotMatch(entity) {
:value="invoiceIn.isBooked"
/>
</QCard>
<!--Vat-->
<QCard v-if="invoiceIn.invoiceInTax.length" class="vn-three">
<a class="header">
{{ t('invoiceIn.card.vat') }}
@ -347,33 +365,54 @@ function setAmountNotMatch(entity) {
hide-pagination
>
<template #header="props">
<QTr :props="props">
<QTr :props="props" class="bg">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</QTh>
</QTr>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd></QTd>
<QTd>{{ toCurrency(invoiceIn.totals.totalTaxableBase) }}</QTd>
<QTd></QTd>
<QTd></QTd>
<QTd></QTd>
<QTd></QTd>
</QTr>
</template>
</QTable>
</QCard>
<!--Due Day-->
<QCard v-if="invoiceIn.invoiceInDueDay.length" class="vn-two">
<div class="header">
{{ t('invoiceIn.card.dueDay') }}
</div>
<QTable
class="full-width"
:columns="dueDayColumns"
:rows="invoiceIn.invoiceInDueDay"
flat
hide-pagination
>
<template #header="props">
<QTr :props="props">
<QTr :props="props" class="bg">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</QTh>
</QTr>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd></QTd>
<QTd></QTd>
<QTd>{{ toCurrency(invoiceIn.totals.totalDueDay) }}</QTd>
<QTd></QTd>
</QTr>
</template>
</QTable>
</QCard>
<!--Intrastat-->
<QCard v-if="invoiceIn.invoiceInIntrastat.length">
<div class="header">
{{ t('invoiceIn.card.intrastat') }}
@ -385,18 +424,30 @@ function setAmountNotMatch(entity) {
hide-pagination
>
<template #header="props">
<QTr :props="props">
<QTr :props="props" class="bg">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</QTh>
</QTr>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd></QTd>
<QTd>{{ toCurrency(intrastatTotals.amount) }}</QTd>
<QTd>{{ intrastatTotals.net }}</QTd>
<QTd>{{ intrastatTotals.stems }}</QTd>
<QTd></QTd>
</QTr>
</template>
</QTable>
</QCard>
</template>
</CardSummary>
</template>
<style lang="scss" scoped>
.bg {
background-color: var(--vn-light-gray);
}
.bordered {
border: 1px solid var(--vn-text);
width: 16em;

View File

@ -82,7 +82,7 @@ const columns = computed(() => [
label: t('Rate'),
sortable: true,
tabIndex: 5,
field: (row) => taxRate(row, row.taxTypeSageFk),
field: (row) => toCurrency(taxRate(row, row.taxTypeSageFk)),
align: 'left',
},
{
@ -119,9 +119,9 @@ function taxRate(invoiceInTax, sageTaxTypeId) {
const taxTypeSage = taxRateSelection && taxRateSelection.rate;
const taxableBase = invoiceInTax && invoiceInTax.taxableBase;
if (taxTypeSage && taxableBase) {
return toCurrency((taxTypeSage / 100) * taxableBase);
return (taxTypeSage / 100) * taxableBase;
}
return toCurrency(0);
return 0;
}
async function addExpense() {
@ -146,10 +146,11 @@ async function addExpense() {
type: 'positive',
message: t('globals.dataSaved'),
});
newExpenseRef.value.hide();
} catch (error) {
quasar.notify({
type: 'negative',
message: t(`${error}`),
message: t(`${error.message}`),
});
}
}
@ -394,7 +395,11 @@ async function addExpense() {
</VnSelectFilter>
</QItem>
<QItem>
{{ taxRate(props.row, props.row.taxTypeSageFk) }}
{{
toCurrency(
taxRate(props.row, props.row.taxTypeSageFk)
)
}}
</QItem>
<QItem>
<QInput

View File

@ -0,0 +1,53 @@
/// <reference types="cypress" />
describe('InvoiceInBasicData', () => {
const selects = '.q-form .q-select';
const appendBtns = 'label button';
const dialogAppendBtns = '.q-dialog label button';
const dialogInputs = '.q-dialog input';
const dialogActionBtns = '.q-card__actions button';
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/invoice-in/1/basic-data`);
});
it('should edit the provideer and supplier ref', () => {
cy.get(selects).eq(0).click();
cy.get(selects).eq(0).type('Bros');
cy.get(selects).eq(0).type('{enter}');
cy.get(appendBtns).eq(0).click();
cy.get('input').eq(2).type(4739);
cy.saveCard();
cy.get(`${selects} input`).eq(0).invoke('val').should('eq', 'Bros nick');
cy.get('input').eq(2).invoke('val').should('eq', '4739');
});
it('should edit the dms data', () => {
const firtsInput = 'Ticket:65';
const secondInput = "I don't know what posting here!";
cy.get(appendBtns).eq(3).click();
cy.get(dialogAppendBtns).eq(0).click();
cy.get(dialogInputs).eq(0).type(firtsInput);
cy.get(dialogAppendBtns).eq(1).click();
cy.get('textarea').type(secondInput);
cy.get(dialogActionBtns).eq(1).click();
cy.get(appendBtns).eq(3).click();
cy.get(dialogInputs).eq(0).invoke('val').should('eq', firtsInput);
cy.get('textarea').invoke('val').should('eq', secondInput);
});
it('should throw an error creating a new dms if a file is not attached', () => {
cy.get(appendBtns).eq(2).click();
cy.get(appendBtns).eq(1).click();
cy.get(dialogActionBtns).eq(1).click();
cy.get('.q-notification__message').should(
'have.text',
"The files can't be empty"
);
});
});

View File

@ -0,0 +1,29 @@
/// <reference types="cypress" />
describe('InvoiceInDueDay', () => {
const inputs = 'label input';
const inputBtns = 'label button';
const dialogBtns = '.q-dialog button';
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/invoice-in/1/due-day`);
});
it('should update the amount', () => {
cy.get(inputBtns).eq(0).click();
cy.get(inputs).eq(3).type(23);
cy.saveCard();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
it('should add a new row ', () => {
cy.addRow();
cy.get(inputs).eq(11).type(8);
cy.saveCard();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
it('should remove the first line', () => {
cy.removeRow(1);
});
});

View File

@ -0,0 +1,38 @@
/// <reference types="cypress" />
describe('InvoiceInIntrastat', () => {
const inputBtns = 'label button';
const thirdRow = 'tbody > :nth-child(3)';
const firstLineCode = 'tbody > :nth-child(1) > :nth-child(2)';
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/invoice-in/1/intrastat`);
});
it('should edit the first line', () => {
cy.selectOption(firstLineCode, 'Plantas vivas: Esqueje/injerto, Vid');
cy.get(inputBtns).eq(1).click();
cy.saveCard();
cy.visit(`/#/invoice-in/1/intrastat`);
cy.getValue(firstLineCode).should(
'have.value',
'Plantas vivas: Esqueje/injerto, Vid'
);
});
it('should add a new row', () => {
const rowData = [false, 'Plantas vivas: Esqueje/injerto, Vid', 30, 10, 5, 'FR'];
cy.addRow();
cy.fillRow(thirdRow, rowData);
cy.saveCard();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
it('should remove the first line', () => {
cy.removeRow(1);
});
});

View File

@ -0,0 +1,55 @@
/// <reference types="cypress" />
describe('InvoiceInVat', () => {
const inputs = 'label input';
const inputBtns = 'label button';
const thirdRow = 'tbody > :nth-child(3)';
const firstLineVat = 'tbody > :nth-child(1) > :nth-child(4)';
const dialogInputs = '.q-dialog label input';
const dialogBtns = '.q-dialog button';
const randomInt = Math.floor(Math.random() * 100);
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/invoice-in/1/vat`);
});
it('should edit the first line', () => {
cy.get(inputBtns).eq(1).click();
cy.get(inputs).eq(2).type(23);
cy.selectOption(firstLineVat, 'H.P. IVA 21% CEE');
cy.saveCard();
cy.visit(`/#/invoice-in/1/vat`);
cy.getValue(firstLineVat).should('have.value', 'H.P. IVA 21% CEE');
});
it('should add a new row', () => {
cy.addRow();
cy.fillRow(thirdRow, [true, 2000000001, 30, 'H.P. IVA 10']);
cy.saveCard();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
it('should remove the first line', () => {
cy.removeRow(1);
});
it('should throw an error if there are fields undefined', () => {
cy.get(inputBtns).eq(0).click();
cy.get(dialogBtns).eq(2).click();
cy.get('.q-notification__message').should('have.text', "The code can't be empty");
});
it('should correctly handle expense addition', () => {
cy.get(inputBtns).eq(0).click();
cy.get(dialogInputs).eq(0).click();
cy.get(dialogInputs).eq(0).type(randomInt);
cy.get(dialogInputs).eq(1).click();
cy.get(dialogInputs).eq(1).type('This is a dummy expense');
cy.get(dialogBtns).eq(2).click();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
});

View File

@ -1,24 +0,0 @@
/// <reference types="cypress" />
describe('InvoiceInBasicData', () => {
const selects = '.q-form .q-select';
const appendBtns = 'label button';
const saveBtn = '.q-btn-group > .q-btn--standard > .q-btn__content > .q-icon';
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/invoice-in/1/basic-data`);
});
it('should edit the provideer and supplier ref', () => {
cy.get(selects).eq(0).click();
cy.get(selects).eq(0).type('Bros');
cy.get(selects).eq(0).type('{enter}');
cy.get(appendBtns).eq(0).click();
cy.get('input').eq(2).type(4739);
cy.get(saveBtn).click();
cy.get(`${selects} input`).eq(0).invoke('val').should('eq', 'Bros nick');
cy.get('input').eq(2).invoke('val').should('eq', 4739);
});
});

View File

@ -104,14 +104,19 @@ Cypress.Commands.add('fillRow', (rowSelector, data) => {
.then((td) => {
if (td.find('.q-select__dropdown-icon').length) {
cy.selectOption(td, value);
}
if (td.find('.q-checkbox__inner').length && value) {
} else if (td.find('.q-checkbox__inner').length && value) {
cy.checkOption(td);
}
} else if (td.find('input[type="text"]') && value)
cy.get(td).find('input').type(value);
});
});
});
Cypress.Commands.add('addRow', () => {
cy.waitForElement('tbody');
cy.get('.q-page-sticky > div > .q-btn > .q-btn__content').click();
});
Cypress.Commands.add('validateRow', (rowSelector, expectedValues) => {
cy.waitForElement('tbody');
cy.get(rowSelector).within(() => {
@ -126,4 +131,26 @@ Cypress.Commands.add('validateRow', (rowSelector, expectedValues) => {
}
});
});
Cypress.Commands.add('removeRow', (rowIndex) => {
let rowsBefore;
let rowsAfter;
cy.get('tr')
.its('length')
.then((length) => {
rowsBefore = length;
cy.get('.q-checkbox').eq(rowIndex).click();
cy.removeCard();
cy.get('.q-dialog button').eq(2).click();
})
.then(() => {
cy.get('tr')
.its('length')
.then((length) => {
rowsAfter = length;
expect(rowsBefore).to.eq(rowsAfter + 1);
});
});
});
// registerCommands();

View File

@ -16,31 +16,35 @@ describe('InvoiceInBasicData', () => {
}).vm;
});
it('should throw an error when data is empty', async () => {
vi.spyOn(axios, 'post').mockResolvedValue({ data: [] });
vi.spyOn(vm.quasar, 'notify');
describe('edit()', () => {
it('should throw an error when data is empty', async () => {
vi.spyOn(axios, 'post').mockResolvedValue({ data: [] });
vi.spyOn(vm.quasar, 'notify');
await vm.edit();
await vm.edit();
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: `The company can't be empty`,
type: 'negative',
})
);
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: `The company can't be empty`,
type: 'negative',
})
);
});
});
it('should throw an error when data is empty', async () => {
vi.spyOn(axios, 'post').mockResolvedValue({ data: [] });
vi.spyOn(vm.quasar, 'notify');
describe('create()', () => {
it('should throw an error when data is empty', async () => {
vi.spyOn(axios, 'post').mockResolvedValue({ data: [] });
vi.spyOn(vm.quasar, 'notify');
await vm.create();
await vm.create();
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: `The company can't be empty`,
type: 'negative',
})
);
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: `The company can't be empty`,
type: 'negative',
})
);
});
});
});

View File

@ -0,0 +1,34 @@
import { vi, describe, expect, it, beforeAll } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import InvoiceInIntrastat from 'src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue';
describe('InvoiceInIntrastat', () => {
let vm;
beforeAll(() => {
vi.spyOn(axios, 'get').mockResolvedValue({ data: [{}] });
vm = createWrapper(InvoiceInIntrastat, {
global: {
stubs: [],
mocks: {
fetch: vi.fn(),
},
},
}).vm;
});
describe('getTotal()', () => {
it('should correctly handle the sum', () => {
vm.invoceInIntrastat = [
{ amount: 10, stems: 162 },
{ amount: 20, stems: 21 },
];
const totalAmount = vm.getTotal('amount');
const totalStems = vm.getTotal('stems');
expect(totalAmount).toBe(10 + 20);
expect(totalStems).toBe(162 + 21);
});
});
});

View File

@ -0,0 +1,75 @@
import { vi, describe, expect, it, beforeAll } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import InvoiceInVat from 'src/pages/InvoiceIn/Card/InvoiceInVat.vue';
describe('InvoiceInVat', () => {
let vm;
beforeAll(() => {
vm = createWrapper(InvoiceInVat, {
global: {
stubs: [],
mocks: {
fetch: vi.fn(),
},
},
}).vm;
});
describe('addExpense()', () => {
beforeAll(() => {
vi.spyOn(axios, 'post').mockResolvedValue({ data: [] });
vi.spyOn(axios, 'get').mockResolvedValue({ data: [] });
vi.spyOn(vm.quasar, 'notify');
});
it('should throw an error when the code property is undefined', async () => {
await vm.addExpense();
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: `The code can't be empty`,
type: 'negative',
})
);
});
it('should correctly handle expense addition', async () => {
vm.newExpense = {
code: 123,
isWithheld: false,
description: 'Descripción del gasto',
};
await vm.addExpense();
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Data saved',
type: 'positive',
})
);
});
});
describe('taxRate()', () => {
it('should correctly compute the tax rate', () => {
const invoiceInTax = { taxableBase: 100 };
const sageTaxTypeId = 1;
vm.sageTaxTypes = [
{ id: 1, rate: 10 },
{ id: 2, rate: 20 },
];
const result = vm.taxRate(invoiceInTax, sageTaxTypeId);
expect(result).toBe((10 / 100) * 100);
});
it('should return 0 if there is not tax rate', () => {
const invoiceInTax = { taxableBase: 100 };
const sageTaxTypeId = 1;
vm.sageTaxTypes = [];
const result = vm.taxRate(invoiceInTax, sageTaxTypeId);
expect(result).toBe(0);
});
});
});