Невозможно прочитать свойство $ i18n из undefined при использовании Vue Test Utils

Я пытаюсь протестировать компонент BaseDialog, который использует VueI18n для переводов с помощью vue-test-utils. Я не могу запустить тест, выполните следующую ошибку:

TypeError: Cannot read property '$i18n' of undefined

    at VueComponent.default (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/src/components/BaseDialog/BaseDialog.vue:2671:220)
    at getPropDefaultValue (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:1662:11)
    at validateProp (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:1619:13)
    at loop (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4612:17)
    at initProps (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4643:33)
    at initState (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4586:21)
    at VueComponent.Vue._init (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4948:5)
    at new VueComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:5095:12)
    at createComponentInstanceForVnode (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:3270:10)
    at init (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:3101:45)
    at createComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:5919:9)
    at createElm (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:5866:9)
    at VueComponent.patch [as __patch__] (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:6416:7)
    at VueComponent.Vue._update (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:3904:19)
    at VueComponent.updateComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4025:10)
    at Watcher.get (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4426:25)
    at new Watcher (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4415:12)
    at mountComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4032:3)
    at VueComponent.Object.<anonymous>.Vue.$mount (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:8350:10)
    at mount (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/@vue/test-utils/dist/vue-test-utils.js:8649:21)
    at shallowMount (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/@vue/test-utils/dist/vue-test-utils.js:8677:10)
    at Object.it (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/src/components/BaseDialog/__tests__/BaseDialog.spec.js:22:21)
    at Object.asyncJestTest (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/jasmine_async.js:108:37)
    at resolve (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/queue_runner.js:56:12)
    at new Promise (<anonymous>)
    at mapper (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/queue_runner.js:43:19)
    at promise.then (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/queue_runner.js:87:41)
    at process.internalTickCallback (internal/process/next_tick.js:77:7)

Я пробовал все решения, перечисленные здесь, но безрезультатно.

Вот соответствующий код:

// BaseDialog.spec.js
import { shallowMount, createLocalVue } from '@vue/test-utils'
import BaseDialog from '@/components/BaseDialog/BaseDialog'
import VueI18n from 'vue-i18n'

describe('BaseDialog', () => {
  it('is called', () => {
    let localVue = createLocalVue()
    localVue.use(VueI18n)
    const messages = {
      gb: {
        'ui.universal.label.ok': 'OK',
        'ui.universal.label.cancel': 'Cancel'
      }
    }

    const i18n = new VueI18n({
      locale: 'gb',
      fallbackLocale: 'gb',
      messages
    })

    const wrapper = shallowMount(BaseDialog, {
      i18n,
      localVue
    })
    expect(wrapper.name()).toBe('BaseDialog')
    expect(wrapper.isVueInstance()).toBeTruthy()
  })
})

// BaseDialog.vue
<template>
  <transition :name="animation">
    <div v-if="isActive" class="dialog modal is-active" :class="size">
      <div class="modal-background" @click="cancel('outside')" />
      <div class="modal-card animation-content">
        <header v-if="title" class="modal-card-head">
          <p class="modal-card-title">{{ title }}</p>
        </header>

        <section
          class="modal-card-body"
          :class="{ 'is-titleless': !title, 'is-flex': hasIcon }"
        >
          <div class="media">
            <div v-if="hasIcon" class="media-left">
              <b-icon
                :icon="icon ? icon : iconByType"
                :pack="iconPack"
                :type="type"
                :both="!icon"
                size="is-large"
              />
            </div>
            <div class="media-content">
              <p v-html="message" />

              <div v-if="hasInput" class="field">
                <div class="control">
                  <input
                    ref="input"
                    v-model="prompt"
                    class="input"
                    :class="{ 'is-danger': validationMessage }"
                    v-bind="inputAttrs"
                    @keyup.enter="confirm"
                  />
                </div>
                <p class="help is-danger">{{ validationMessage }}</p>
              </div>
            </div>
          </div>
        </section>

        <footer class="modal-card-foot">
          <button
            v-if="showCancel"
            ref="cancelButton"
            class="button"
            @click="cancel('button')"
          >
            {{ cancelText }}
          </button>
          <button
            ref="confirmButton"
            class="button"
            :class="type"
            @click="confirm"
          >
            {{ confirmText }}
          </button>
        </footer>
      </div>
    </div>
  </transition>
</template>

<script>
import Modal from '../BaseModal/BaseModal'
import config from '../../utils/config'
import { removeElement } from '../../utils/helpers'

export default {
  name: 'BaseDialog',
  extends: Modal,
  props: {
    title: {
      type: String,
      default: null
    },
    message: {
      type: String,
      default: null
    },
    icon: {
      type: String,
      default: null
    },
    iconPack: {
      type: String,
      default: null
    },
    hasIcon: {
      type: Boolean,
      default: false
    },
    type: {
      type: String,
      default: 'is-primary'
    },
    size: {
      type: String,
      default: null
    },
    confirmText: {
      type: String,
      default: () => {
        return config.defaultDialogConfirmText
          ? config.defaultDialogConfirmText
          : this.$i18n('ui.universal.label.ok')
      }
    },
    cancelText: {
      type: String,
      default: () => {
        return config.defaultDialogCancelText
          ? config.defaultDialogCancelText
          : this.$i18n('ui.universal.label.cancel')
      }
    },
    hasInput: Boolean, // Used internally to know if it's prompt
    inputAttrs: {
      type: Object,
      default: () => ({})
    },
    onConfirm: {
      type: Function,
      default: () => {}
    },
    focusOn: {
      type: String,
      default: 'confirm'
    }
  },
  data() {
    const prompt = this.hasInput ? this.inputAttrs.value || '' : ''

    return {
      prompt,
      isActive: false,
      validationMessage: ''
    }
  },
  computed: {
    /**
     * Icon name (MDI) based on the type.
     */
    iconByType() {
      switch (this.type) {
        case 'is-info':
          return 'information'
        case 'is-success':
          return 'check-circle'
        case 'is-warning':
          return 'alert'
        case 'is-danger':
          return 'alert-circle'
        default:
          return null
      }
    },
    showCancel() {
      return this.cancelOptions.indexOf('button') >= 0
    }
  },
  beforeMount() {
    // Insert the Dialog component in body tag
    this.$nextTick(() => {
      document.body.appendChild(this.$el)
    })
  },
  mounted() {
    this.isActive = true

    if (typeof this.inputAttrs.required === 'undefined') {
      this.$set(this.inputAttrs, 'required', true)
    }

    this.$nextTick(() => {
      // Handle which element receives focus
      if (this.hasInput) {
        this.$refs.input.focus()
      } else if (this.focusOn === 'cancel' && this.showCancel) {
        this.$refs.cancelButton.focus()
      } else {
        this.$refs.confirmButton.focus()
      }
    })
  },
  methods: {
    /**
     * If it's a prompt Dialog, validate the input.
     * Call the onConfirm prop (function) and close the Dialog.
     */
    confirm() {
      if (this.$refs.input !== undefined) {
        if (!this.$refs.input.checkValidity()) {
          this.validationMessage = this.$refs.input.validationMessage
          this.$nextTick(() => this.$refs.input.select())
          return
        }
      }

      this.onConfirm(this.prompt)
      this.close()
    },

    /**
     * Close the Dialog.
     */
    close() {
      this.isActive = false
      // Timeout for the animation complete before destroying
      setTimeout(() => {
        this.$destroy()
        removeElement(this.$el)
      }, 150)
    }
  }
}
</script>
<style lang="scss">
.dialog {
  .modal-card {
    max-width: 460px;
    width: auto;
    .modal-card-head {
      font-size: $size-5;
      font-weight: $weight-semibold;
    }
    .modal-card-body {
      .field {
        margin-top: 16px;
      }
    }
    .modal-card-foot {
      justify-content: flex-end;
      .button {
        display: inline; // Fix Safari centering
        min-width: 5em;
        font-weight: $weight-semibold;
      }
    }
    @include tablet {
      min-width: 320px;
    }
  }
  &.is-small {
    .modal-card,
    .input,
    .button {
      @include control-small;
    }
  }
  &.is-medium {
    .modal-card,
    .input,
    .button {
      @include control-medium;
    }
  }
  &.is-large {
    .modal-card,
    .input,
    .button {
      @include control-large;
    }
  }
}
</style>

Я действительно не знаю, что еще здесь попробовать. Это начало проекта, в котором я должен поддерживать 9 языков с более чем 500 ключами на штуку, так что мне нужно заставить это работать. Любая помощь очень ценится.


person MMelvin0581    schedule 25.02.2019    source источник


Ответы (1)


Проблема была в том, что я ссылался на this в реквизите. Реквизиты обрабатываются до создания экземпляра компонента, поэтому у меня не было доступа к this. Всегда из-за мелочей ты бьешься головой об стену, хахаха.

person MMelvin0581    schedule 01.03.2019