Модульное тестирование компонентов vue с проблемой Jest

#javascript #unit-testing #vue.js #jestjs #vue-component

#javascript #модульное тестирование #vue.js #jestjs #vue-компонент

Вопрос:

Я изо всех сил пытаюсь выполнить модульное тестирование некоторых компонентов в приложении, и я был бы очень признателен, если бы кто-нибудь мог мне помочь. Я уже настроил среду Jest для тестирования в Vue, и все работает нормально, пока я не попытаюсь протестировать компонент, который полагается на другие модули с функциями в нем. Эти функции являются помощниками, полезными для более быстрого импорта действий vuex и геттеров в компоненты. Здесь есть несколько строк кода, которые, я надеюсь, помогут решить проблему:

  • это ошибка, которую Jest выдает в консоли:
 /app # yarn test:unit /common / page - header / ProjectWizard.spec.js
yarn run v1 .22 .5
$ jest / common / page - header / ProjectWizard.spec.js
FAIL src / specs / common / page - header / projectWizard.spec.jsTest suite failed to run

Configuration error:

  Could not locate module @ / state / helpers mapped as:
  /app/$1.

Please check your configuration
for these entries: {
  "moduleNameMapper": {
    "/^@/(.*)$/": "/app/$1"
  },
  "resolver": undefined
}

57 |
  import VueGoogleAutocomplete from '../../../node_modules/vue-google-autocomplete'
58 |
  > 59 |
  import {
    |
    ^
    60 | contractTypesMethods,
    61 | contractTypesGetters,
    62 | installationTypesMethods,

    at createNoMappedModuleFoundError(node_modules / jest - resolve / build / index.js: 551: 17)
    at src / components / builder / ProjectWizard.vue: 59: 1
    at Object. < anonymous > (src / components / builder / ProjectWizard.vue: 491: 3)
 
  • это файл ProjectWizard:
 <template>
  <form-wizard
    @on-complete="onComplete(model)"
    error-color="#f1b44c"
    color="#EC641C"
    nextButtonText="Avanti"
    backButtonText="Indietro"
    :finishButtonText="(isModifying) ? 'Conferma modifiche' : 'Crea Progetto'"
    id="project-form-wizard"
    style="overflow: hidden;"
  >

    <tab-content title="Informazioni preliminari" icon="mdi mdi-folder-information" :before-change="validateFirstTab">

      <vue-form-generator-component :model="model" :schema="zeroTabSchema" :options="formOptions" ref="zeroTabSchema">
      </vue-form-generator-component>

      <b-form-group featured class="col-12" label="Indirizzo (Via etc.)" label-for="indirizzo-progetto">
        <vue-google-autocomplete required keypress types="" ref="address" id="indirizzo-progetto"
          classname="form-control" placeholder="Inserisci qui l'indirizzo del progetto"
          v-on:placechanged="getAddressData" country="it">
        </vue-google-autocomplete>
      </b-form-group>

      <vue-form-generator-component :model="model" :schema="firstTabSchema" :options="formOptions" ref="firstTabForm">
      </vue-form-generator-component>

    </tab-content>
    <tab-content 
      title="Dettagli progetto"
      icon="mdi mdi-home-search-outline" 
      :before-change="validateSecondTab"
    >

      <vue-form-generator-component
        :model="model" 
        :schema="secondTabSchema"
        :options="formOptions" 
        ref="secondTabForm"
      ></vue-form-generator-component>

    </tab-content>
  </form-wizard>
</template>

<script>
import {
  FormWizard,
  TabContent
} from "vue-form-wizard";

import VueFormGenerator from 'vue-form-generator'
import {
  resources
} from "../../../src/utils/validation"
Object.assign(VueFormGenerator.validators.resources, resources)
import VueGoogleAutocomplete from '../../../node_modules/vue-google-autocomplete'

import {
  contractTypesMethods,
  contractTypesGetters,
  installationTypesMethods,
  installationTypesGetters,
  interventionTypesMethods,
  interventionTypesGetters,
  contactsMethods,
  projectManagementMethods,
  projectManagementGetters,
  projectsMethods
} from "@/state/helpers";

// STATELESS
import {
  fetchContacts
} from '@/stateless/modules/common'

export default {
  data() {
    var vm = this;
    return {
      ...contractTypesGetters,
      ...installationTypesGetters,
      ...interventionTypesGetters,
      ...projectManagementGetters,
      
      contractTypes: [],
      selectedContract: "",
      installationTypesFiltered: [],
      interventionTypesFiltered: [],
      installationTypes: [],
      interventionTypes: [],
      contactPersons: [],
      address: '',
      model: {
        projectName: '',
        address: {
          partialStreet: '',
          streetNumber: '',
          city: '',
          province: '',
          country: '',
          CAP: '',
          state: ''
        },
        contractType: '',
        installationType: '',
        interventionType: '',
        contactPerson: '',
        description: ''
      },
      formOptions: {
        validationErrorClass: "has-error",
        validationSuccessClass: "has-success",
        validateAfterChanged: true,
      },
      zeroTabSchema: {
        fields: [{
            type: "input",
            inputType: "text",
            label: "Nome del progetto",
            model: "projectName",
            required: true,
            featured: true,
            maxlength: 64,
            validator: VueFormGenerator.validators.string,
            styleClasses: 'col-12'
          },]
      },
      firstTabSchema: {
        fields: [
          {
            type: "input",
            inputType: "text",
            label: "Via",
            model: "address.partialStreet",
            required: true,
            featured: true,
            maxlength: 128,
            validator: VueFormGenerator.validators.string,
            styleClasses: 'col-6'
          },
          {
            type: "input",
            inputType: "text",
            label: "Numero civico",
            model: "address.streetNumber",
            required: true,
            featured: true,
            maxlength: 16,
            validator: VueFormGenerator.validators.string,
            styleClasses: 'col-6'
          },
          {
            type: "input",
            inputType: "text",
            label: "Città",
            model: "address.city",
            required: true,
            featured: true,
            maxlength: 64,
            validator: VueFormGenerator.validators.string,
            styleClasses: 'col-6'
          },
          {
            type: "input",
            inputType: "text",
            label: "Provincia",
            model: "address.province",
            required: true,
            featured: true,
            maxlength: 2,
            validator: VueFormGenerator.validators.string,
            styleClasses: 'col-6'
          },
          {
            type: "input",
            inputType: "text",
            label: "Paese",
            model: "address.country",
            required: true,
            featured: true,
            maxlength: 32,
            validator: VueFormGenerator.validators.string,
            styleClasses: 'col-6'
          },
          {
            type: "input",
            inputType: "text",
            label: "CAP",
            model: "address.CAP",
            required: true,
            featured: true,
            min: 5,
            maxlength: 8,
            validator: VueFormGenerator.validators.string,
            styleClasses: 'col-6'
          },
          {
            type: "input",
            inputType: "text",
            visible: false,
            label: "Regione",
            model: "address.state",
            required: true,
            featured: true,
            maxlength: 32,
            validator: VueFormGenerator.validators.string,
            styleClasses: 'col-6'
          },
        ]
      },
      secondTabSchema: {
        fields: [{
            type: "select",
            label: "Tipo di appalto",
            model: "contractType",
            required: true,
            featured: true,
            validator: VueFormGenerator.validators.string,
            styleClasses: 'col-12',
            selectOptions: {
              hideNoneSelectedText: true
            },
            onChanged(event) {
              const filterdTypes = vm.installationTypes.filter((element) => {
                return element.contractType === event.contractType
              })
              let installation = document.getElementById('insediamento')
              installation.innerHTML = ''
              installation.disabled = false
              for (let i = 0; i < filterdTypes.length; i  ) {
                installation.options[i] = new Option(
                  filterdTypes[i].name, 
                  filterdTypes[i].id
                )
              }
              installation.value = ""
              let intervention = document.getElementById('tipologia-di-intervento')
              intervention.value = ""
            },
            values: () => {
              return this.contractTypes
            },
          },
          {
            type: "select",
            disabled: "true",
            label: "Insediamento",
            model: "installationType",
            required: true,
            featured: true,
            validator: VueFormGenerator.validators.string,
            styleClasses: 'col-12',
            selectOptions: {
              hideNoneSelectedText: true
            },
            onChanged(event) {
              const filterdTypes = vm.interventionTypes.filter((element) => {
                return element.installationType === event.installationType
              })
              let intervention = document.getElementById('tipologia-di-intervento')
              intervention.innerHTML = ''
              intervention.disabled = false
              for (let i = 0; i < filterdTypes.length; i  ) {
                intervention.options[i] = new Option(
                  filterdTypes[i].name, 
                  filterdTypes[i].id
                )
              }
              intervention.value = ""
            },
            values: () => {
              return this.isModifying ? this.installationTypes : []
            },
          },
          {
            type: "select",
            disabled: "true",
            noneSelectedText: "",
            label: "Tipologia di intervento",
            model: "interventionType",
            required: true,
            featured: true,
            validator: VueFormGenerator.validators.string,
            styleClasses: 'col-12',
            selectOptions: {
              hideNoneSelectedText: true
            },
            values: () => {
              return this.isModifying ? this.interventionTypes : []
            },
          },
          {
            type: "select",
            label: "Referente di progetto",
            model: "contactPerson",
            required: true,
            featured: true,
            validator: VueFormGenerator.validators.string,
            styleClasses: 'col-12',
            selectOptions: {
              hideNoneSelectedText: true
            },
            hint: '<div id="new-contact-creation-project"><i class="mdi mdi-account-multiple-plus"></i> <span>Crea nuovo referente</span></div>',
            values: () => {
              return this.contactPersons
            },
          },
          {
            type: "textArea",
            label: "Descrizione",
            model: "description",
            featured: true,
            maxlength: 512,
            validator: VueFormGenerator.validators.string,
            styleClasses: 'col-12'
          }
        ]
      }
    }
  },

  components: {
    FormWizard,
    TabContent,
    'vue-form-generator-component': VueFormGenerator.component,
    VueGoogleAutocomplete
  },

  computed: {
    isModifying() {
       return (this.$router.currentRoute.name == 'builderHome') ? false : true
    },
  },

  methods: {
    ...contractTypesMethods,
    ...installationTypesMethods,
    ...interventionTypesMethods,
    ...contactsMethods,
    ...projectsMethods,
    ...projectManagementMethods,

    onComplete: function (json) {
      if (this.isModifying) {
        this.updateProject({
          projectId: this.currentProject().id,
          name: this.model.projectName,
          description: this.model.description,
          address: this.model.address.partialStreet,
          streetNumber: this.model.address.streetNumber,
          city: this.model.address.city,
          province: this.model.address.province,
          country: this.model.address.country,
          zipCode: this.model.address.CAP,
          contactPerson: this.model.contactPerson,
          contractType: this.model.contractType,
          installationType: this.model.installationType,
          interventionType: this.model.interventionType
        }).then(() => location.reload())
      } else {
        this.addProject(json)
        .then(response => {
          if (response) {
            this.$router.push({
                  name: 'BOMUpload',
                  params: { projectId: response.data.data.createProject.project.id }
              })
          }
        })
      }
    },
    validateFirstTab: function () {
      if (!this.model.projectName) {
        return alert('Il nome del progetto è obbligatorio')
      }
      return this.$refs.firstTabForm.validate();
    },
    validateSecondTab: function () {
      return this.$refs.secondTabForm.validate();
    },

    newContactPersonCreation() {
      this.$emit('new-contact-person-creation')
    },
    
    getAddressData: function (addressData, placeResultData) {
      this.address = addressData;
      this.model.address.country = addressData.country;
      this.model.address.city = addressData.locality;
      if (placeResultData.address_components[4].short_name == 'IT') {
        this.model.address.province = placeResultData.address_components[2].short_name
      } else if (placeResultData.address_components[4].short_name.length > 2) {
        this.model.address.province = placeResultData.address_components[3].short_name
      } else {
        this.model.address.province = placeResultData.address_components[4].short_name
      }
      this.model.address.partialStreet = addressData.route;
      this.model.address.streetNumber = addressData.street_number;
      this.model.address.CAP = addressData.postal_code;
      this.model.address.state = addressData.administrative_area_level_1;
    },

    getContacts() {
      fetchContacts()
      .then(response => {
        this.contactPersons = response.map(contact => {
          return { id: contact.node.id, name: contact.node.name }
        })
        this.model.contactPerson = this.contactPersons.slice(-2).pop().id
        if (this.isModifying) {
          this.model.contactPerson = this.currentProject().contactPerson.id
        }
      })
      .catch(err => console.error('Riga 417 ProjectWizard.vue', err))
    }
  },

  mounted() {

    if (this.isModifying) {
      this.model.projectName = this.currentProject().name
      this.model.address.partialStreet = this.currentProject().address.address
      this.model.address.streetNumber = this.currentProject().address.streetNumber
      this.model.address.city = this.currentProject().address.city
      this.model.address.province = this.currentProject().address.province
      this.model.address.country = this.currentProject().address.country
      this.model.address.CAP = this.currentProject().address.zipCode      
      this.model.description = this.currentProject().description
    }

    var newContact = document.querySelector("#new-contact-creation-project")
    newContact.onclick = () => {
      this.newContactPersonCreation()
    };

    // CONTRACT TYPES
    this.fetchContractTypes()
      .then(response => {
        if (response) {
          let processedContractTypes = []
          this.contractTypes = this.allContractTypes()
      
          this.contractTypes.forEach(contractType => {
            let contractTypeObj = { id: contractType.node.id, name: contractType.node.name }
            processedContractTypes.push(contractTypeObj)
          })

          this.contractTypes = processedContractTypes

          if (this.isModifying){
            this.model.contractType = this.currentProject().contractType.id
          }

        } else {
          alert('Not working fetchContractTypes')
        }
      })

    // INSTALLATION TYPES
    this.fetchInstallationTypes()
      .then(response => {
        if (response) {
          let processedInstallationTypes = []
          this.installationTypes = this.allInstallationTypes()

          this.installationTypes.forEach(installationType => {
            let installationTypeObj = { 
              id: installationType.node.id, 
              name: installationType.node.name,
              contractType: installationType.node.contractType.id
            }
            processedInstallationTypes.push(installationTypeObj)
          })

          this.installationTypes = processedInstallationTypes

          if (this.isModifying){
            this.model.installationType = this.currentProject().installationType.id
          }

        } else {
          alert('Not working fetchInstallationTypes')
        }
      })

    // INTERVENTION TYPES
    this.fetchInterventionTypes()
      .then(response => {
        if (response) {
          let processedInterventionTypes = []
          this.interventionTypes = this.allInterventionTypes()

          this.interventionTypes.forEach(interventionType => {
            let interventionTypeObj = {
              id: interventionType.node.id,
              name: interventionType.node.name,
              installationType: interventionType.node.installationType.id,
            }
            processedInterventionTypes.push(interventionTypeObj)
          })

          this.interventionTypes = processedInterventionTypes

          if (this.isModifying){
            this.model.interventionType = this.currentProject().interventionType.id
          }

        } else {
          alert('Not working fetchInterventionTypes')
        }
      })

    // CONTACT PERSONS
    this.getContacts()
  },
}
</script>
 
  • это мой файл конфигурации jest:
 module.exports = {
  verbose: true,
  roots: ["<rootDir>/src/", "<rootDir>/src/specs/"],
  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
  moduleNameMapper: {
    '^@/(.*)

  • и, наконец, это код в файле спецификации:
 import { mount } from '@vue/test-utils'
import Vue from 'vue'
import BootstrapVue from 'bootstrap-vue'
import Component from '@/src/components/builder/ProjectWizard'

Vue.use(BootstrapVue)

describe('ProjectWizard', () => {
  it('should render a card for each project received from its parent', () => {
    const wrapper = mount(Component)
    console.log(wrapper)
  })
})
 

Я не понимаю, почему Jest не может найти эти функции, поскольку местоположение модуля правильное.

Комментарии:

1. Может быть, предоставить код ProjectWizard.vue ?

2. импорт или запрос from node_modules с относительным путем является нестандартным и избыточным и может создавать проблемы с webpack и вызывать проблемы. Я бы попробовал import VueGoogleAutocomplete from 'vue-google-autocomplete' вместо этого.

: '<rootDir>/$1',
"~(.*)$": "<rootDir>/$1"
},
transform: {
"^. \.js$": "babel-jest",
'. \.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2|svg)

  • и, наконец, это код в файле спецификации:

Я не понимаю, почему Jest не может найти эти функции, поскольку местоположение модуля правильное.

Комментарии:

1. Может быть, предоставить код ProjectWizard.vue ?

2. импорт или запрос from node_modules с относительным путем является нестандартным и избыточным и может создавать проблемы с webpack и вызывать проблемы. Я бы попробовал import VueGoogleAutocomplete from 'vue-google-autocomplete' вместо этого.

: 'jest-transform-stub',
"^. \.vue$": "vue-jest"
},
snapshotSerializers: [
"<rootDir>/node_modules/jest-serializer-vue"
],
transformIgnorePatterns: [
'<rootDir>/node_modules/(?!vue-google-autocomplete)',
'<rootDir>/(?!)'
]
}

  • и, наконец, это код в файле спецификации:

Я не понимаю, почему Jest не может найти эти функции, поскольку местоположение модуля правильное.

Комментарии:

1. Может быть, предоставить код ProjectWizard.vue ?

2. импорт или запрос from node_modules с относительным путем является нестандартным и избыточным и может создавать проблемы с webpack и вызывать проблемы. Я бы попробовал import VueGoogleAutocomplete from 'vue-google-autocomplete' вместо этого.