Тестирование Vue с Vuetify. Невозможно прочитать свойство 'title' из undefined

Я пытаюсь проверить, будет ли диалоговое окно, созданное с помощью vuetify, активным после того, как я испущу функцию под названием closeNoteForm. Однако, когда я пытаюсь проверить, скрыто ли содержимое диалогового окна, я получаю сообщение об ошибке. Кажется, проблема в оболочке noteForm, которую я создал. Но это просто указывает на css и не имеет для меня особого смысла.

В моем компоненте NoteForm

<template>
  <v-card class="note-form-container">
    <v-text-field
      v-model="title"
      placeholder="Note Title"
      hide-details
      class="font-weight-bold text-h5 pa-2"
      flat
      solo
      color="yellow"
    >
    </v-text-field>

    <vue-tags-input
      v-model="tag"
      :tags="tags"
      @tags-changed="(newTags) => (tags = newTags)"
    />

    <div class="mt-2">
      <input
        type="file"
        id="uploadImg"
        style="display: none"
        multiple
        accept="image/*"
        v-on:change="handleFileUploads"
      />
      <label class="text-button pa-2 upload__label ml-3 mt-2" for="uploadImg"
        >Upload Image</label
      >
    </div>
    <v-container>
      <v-row justify="space-around"
        ><div v-for="(image, index) in allImages" :key="index">
          <div style="position: relative">
            <v-img
              :src="image"
              height="70px"
              width="70px"
              contain
              class="mx-2 uploaded__image"
              @click="openImage(image)"
            ></v-img>
            <v-btn
              icon
              color="pink"
              class="close__button"
              @click="handleDeleteButton(image)"
            >
              <v-icon>mdi-close</v-icon>
            </v-btn>
          </div>
        </div>
        <v-spacer></v-spacer>
      </v-row>
    </v-container>

    <v-textarea
      v-model="text"
      clearable
      clear-icon="mdi-close-circle"
      no-resize
      hide-details
      solo
      flat
    ></v-textarea>

    <v-card-actions>
      <v-spacer></v-spacer>
      <v-btn text @click="closeForm()" class="close__btn"> Close </v-btn>
      <v-btn color="secondary darken-2" text @click="saveNote"> Save </v-btn>
    </v-card-actions>
    <v-dialog v-model="dialog" width="500" dark>
      <v-img :src="selectedImage" @click="dialog = false"></v-img>
    </v-dialog>
    <v-dialog
      v-model="imageDeletionDialog"
      width="500"
      class="image_delete_dialog"
    >
      <v-img :src="selectedImage"></v-img>
      <v-btn color="red darken-1" text @click="deleteImage">
        Delete Image
      </v-btn>
      <v-btn text @click="imageDeletionDialog = false"> Close </v-btn>
    </v-dialog>
  </v-card>
</template>
  
<script>
import { EventBus } from "../event-bus";
import VueTagsInput from "@johmun/vue-tags-input";
import { v4 as uuidv4 } from "uuid";
import dbService from "../services/db_service";

export default {
  name: "NoteForm",
  components: { VueTagsInput },

  data: () => {
    return {
      text: "",
      title: "",
      tag: "",
      tags: [],
      currentNoteID: null,
      allImages: [],
      dialog: false,
      selectedImage: "",
      imageDeletionDialog: false,
    };
  },

  mounted() {
    EventBus.$on("editNote", (noteToEdit) => {
      this.fillNoteForm(noteToEdit);
    });
  },
  methods: {
    openImage(image) {
      this.selectedImage = image;
      this.dialog = true;
    },
    handleDeleteButton(image) {
      this.imageDeletionDialog = true;
      this.selectedImage = image;
    },
    deleteImage() {
      this.allImages = this.allImages.filter(
        (img) => img !== this.selectedImage
      );
      this.imageDeletionDialog = false;
      this.selectedImage = "";
    },
    handleFileUploads(e) {
      const images = e.target.files;
      let imageArray = [];
      for (let i = 0; i < images.length; i++) {
        const reader = new FileReader();
        const image = images[i];
        reader.onload = () => {
          imageArray.push(reader.result);
        };
        reader.readAsDataURL(image);
      }
      this.allImages = imageArray;

      e.target.value = "";
    },

    saveNote() {
      if (this.title.trim() === "") {
        alert("Please enter note title!");
        return;
      }
      if (this.text === null) this.text = "";
      if (this.currentNoteID === null) {
        this.createNewNote();
      } else {
        this.updateNote();
      }
      this.resetForm();
    },
    createNewNote() {
      const tagList = this.tags.map((tag) => {
        return tag.text;
      });
      const uuid = uuidv4();
      const newNote = {
        title: this.title,
        tags: this.tags,
        text: this.text,
        uuid,
        date: new Date().toLocaleString(),
        tagList,
        allImages: this.allImages,
      };

      dbService.addNote(newNote);
      EventBus.$emit("addNewNote", newNote);
      EventBus.$emit("closeNoteForm");
    },
    updateNote() {
      const tagList = this.tags.map((tag) => {
        return tag.text;
      });
      let updatedNote = {
        tags: this.tags,
        uuid: this.currentNoteID,
        text: this.text,
        title: this.title,
        tagList: tagList,
        allImages: this.allImages,
      };
      dbService.updateNote(updatedNote);
      EventBus.$emit("updateNote", updatedNote);
    },
    resetForm() {
      this.tags = [];
      this.text = "";
      this.title = "";
      this.currentNoteID = null;
      this.allImages = [];
      this.selectedImage = "";
    },
    closeForm() {
      if (this.currentNoteID !== null) {
        EventBus.$emit("openNoteView", this.currentNoteID);
      }
      this.resetForm();
      **EventBus.$emit("closeNoteForm");**
    },
    fillNoteForm(noteToEdit) {
      this.text = noteToEdit.text;
      this.title = noteToEdit.title;
      this.tags = noteToEdit.tags;
      this.currentNoteID = noteToEdit.uuid;
      this.allImages = noteToEdit.allImages;
    },
  },
  beforeDestroy() {
    EventBus.$off("fillNoteForm", this.fillNoteForm);
  },
};
</script>

<style lang="scss" >
.uploaded__image {
  position: relative;
  cursor: pointer;
}
.close__button {
  position: absolute;
  top: 0;
  right: 0;
}

.upload__label {
  background-color: gray;
  cursor: pointer;
  &:hover {
    background-color: lightgrey;
  }
}
.note-form-container {
  scrollbar-width: none;
}
.v-input__control,
.v-input__slot,
.v-text-field__slot {
  height: 100% !important;
}
.v-textarea {
  height: 350px !important;
}
.vue-tags-input {
  max-width: 100% !important;
  border: none;
  background: transparent !important;
}
.v-dialog {
  background-color: rgb(230, 230, 230);
}

.vue-tags-input .ti-tag {
  position: relative;
}

.vue-tags-input .ti-input {
  padding: 4px 10px;
  transition: border-bottom 200ms ease;
  border: none;
  height: 50px;
  overflow: auto;
}

.note-form-container.theme--dark {
  .vue-tags-input .ti-tag {
    position: relative;
    background: white;
    color: black !important;
  }
  .vue-tags-input .ti-new-tag-input {
    color: #07c9d2;
  }
}
.note-form-container.theme--light {
  .vue-tags-input .ti-tag {
    position: relative;
    background: black;
    color: white !important;
  }
  .vue-tags-input .ti-new-tag-input {
    color: #085e62;
  }
}
</style>

Компонент "Моя заметка"

 <template>
  <v-dialog width="500px" v-model="dialog">
    <template v-slot:activator="{ on, attrs }">
      <v-card
        v-bind="attrs"
        v-on="on"
        height="200px"
        color="primary"
        class="note_card"
      >
        <v-card-title class="text-header font-weight-bold white--text"
          >{{ note.title }}
        </v-card-title>

        <v-card-subtitle
          v-if="note.text.length < 150"
          class="text-caption white--text note__subtitle"
        >
          {{ note.text }}
        </v-card-subtitle>
        <v-card-subtitle v-else class="text-caption note__subtitle">
          {{ note.text.substring(0, 150) + ".." }}
        </v-card-subtitle>
      </v-card>
    </template>
    <template>
      <v-card class="read_only_note">
        <v-card-subtitle class="text-h4 black--text font-weight-bold pa-5"
          >{{ note.title }}
        </v-card-subtitle>
        <v-card-subtitle>
          <span
            v-for="(note, index) in this.note.tagList"
            :key="index"
            class="tag_span"
          >
            {{ note }}
          </span>
        </v-card-subtitle>
        <v-container>
          <v-row justify="space-around" class="note__images"
            ><div v-for="(image, index) in note.allImages" :key="index">
              <v-img
                :src="image"
                height="70px"
                width="70px"
                contain
                class="mx-2"
                @click="openImage(image)"
              ></v-img>
            </div>
            <v-spacer></v-spacer>
          </v-row>
        </v-container>
        <v-card-subtitle class="text__container">
          <p v-html="convertedText" @click="detectYoutubeClick"></p>
        </v-card-subtitle>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn color="red darken-1" text @click="deleteDialog = true">
            Delete
          </v-btn>
          <v-btn color="secondary darken-2" text @click="closeNoteView()">
            Close
          </v-btn>
          <v-btn color="secondary darken-2" text @click="editNote">
            Edit
          </v-btn>
        </v-card-actions>
      </v-card>
    </template>

    <v-dialog v-model="imageDialog" width="500">
      <v-img :src="selectedImage" @click="imageDialog = false"></v-img>
    </v-dialog>
    <v-dialog v-model="deleteDialog" width="500">
      <h4 style="text-align: center" class="pa-5">
        Are you sure you want to delete {{ note.title }}?
      </h4>
      <v-btn color="red darken-1" text @click="deleteNote()"> Delete </v-btn>
      <v-btn color="blue darken-1" text @click="deleteDialog = false">
        Close
      </v-btn>
    </v-dialog>
    <v-dialog v-model="youtubeDialog" width="500">
      <iframe
        id="ytplayer"
        type="text/html"
        width="500"
        height="400"
        :src="youtubeSrc"
        frameborder="0"
      ></iframe>
    </v-dialog>
  </v-dialog>
</template>

<script>
import { EventBus } from "../event-bus";
export default {
  props: {
    note: Object,
  },

  data: () => {
    return {
      dialog: false,
      imageDialog: false,
      deleteDialog: false,
      selectedImage: "",
      youtubeDialog: false,
      youtubeSrc: "",
    };
  },
  mounted() {
    EventBus.$on("openNoteView", (noteID) => {
      if (this.note.uuid === noteID) {
        return this.openNoteView();
      }
    });
  },
  methods: {
    openImage(image) {
      this.selectedImage = image;
      this.imageDialog = true;
    },
    deleteNote() {
      EventBus.$emit("deleteNote", this.note.uuid);
    },
    editNote() {
      EventBus.$emit("openNoteForm");
      this.closeNoteView();
      // To load data after note form is mounted
      setTimeout(() => {
        EventBus.$emit("editNote", this.note);
      }, 200);
    },
    openNoteView() {
      this.dialog = true;
    },
    closeNoteView() {
      this.dialog = false;
    },
    detectYoutubeClick(e) {
      if (e.target.innerText.includes("youtube")) {
        const url = e.target.innerText.replace("watch?v=", "embed/");
        this.youtubeSrc = !url.includes("http") ? "https://" + url : url;
        this.youtubeDialog = true;
      }
    },
  },
  computed: {
    convertedText: function () {
      const urlRegex = /(((https?:\/\/)|(www\.))[^\s]+)/g;
      return this.note.text.replace(urlRegex, function (url, b, c) {
        const url2 = c == "www." ? "http://" + url : url;
        if (url2.includes("youtube")) {
          return `<span class="youtube_url">${url}</span>`;
        } else {
          return '<a href="' + url2 + '" target="_blank">' + url + "</a>";
        }
      });
    },
  },
  watch: {
    youtubeDialog: function (newVal) {
      if (newVal === false) this.youtubeSrc = "";
    },
  },
  beforeDestroy() {
    EventBus.$off("openNoteView", this.openNoteView);
  },
};
</script>

<style lang="scss" >
.text__container {
  height: 50vh;
  p {
    width: 100%;
  }
}
.note__images {
  margin-left: 5px !important;
}
.note_card {
  border: 1px solid black !important;
}
.theme--dark.v-card .v-card__title {
  color: black !important;
}
.theme--dark.v-card .v-card__subtitle.note__subtitle {
  color: black !important;
}
.theme--light.v-card .v-card__subtitle.note__subtitle {
  color: white !important;
}
.read_only_note.theme--dark {
  .v-card__subtitle {
    color: white !important;
    display: flex;
    flex-wrap: wrap;
  }
  .tag_span {
    background-color: white;
    color: black !important;
    padding: 2px;
    border-radius: 3px;
    margin: 5px;
  }
}
.read_only_note.theme--light {
  .v-card__subtitle {
    display: flex;
    flex-wrap: wrap;
  }
  .tag_span {
    color: white !important;
    background-color: #212121;
    padding: 2px;
    border-radius: 3px;
    margin: 5px;
  }
}
.youtube_url {
  text-decoration: underline;
  &:hover {
    cursor: pointer;
  }
}
</style>

Мой файл спецификаций

import Vue from 'vue';
Vue.use(Vuetify);
import Vuetify from 'vuetify';
import NoteForm from '@/components/NoteForm';
import Note from '@/components/Note';
import { createLocalVue, mount } from '@vue/test-utils';

describe('NoteForm.vue', () => {
  const localVue = createLocalVue();
  localVue.use(Vuetify);
  document.body.setAttribute('data-app', true);
  let vuetify;

  beforeEach(() => {
    vuetify = new Vuetify();
  });

  it('should emit an event when the action v-btn is clicked', async () => {
    const formWrapper = mount(NoteForm, {
      localVue,
      vuetify,
    });

    const noteWrapper = mount(Note, {
      localVue,
      vuetify,
    });

    formWrapper.vm.$emit('closeNoteForm');
    await formWrapper.vm.$nextTick(); // Wait until $emits have been handled
    expect(formWrapper.emitted().closeNoteForm).toBeTruthy();
    expect(noteWrapper.find('.read_only_note').isVisible()).toBe(true);

  });
});

Однако я получаю сообщение об ошибке

 [Vue warn]: Error in render: "TypeError: Cannot read property 'title' of undefined"
    
    found in
    
    ---> <Anonymous>
           <Root>

  console.error node_modules/vue/dist/vue.runtime.common.dev.js:1884
    TypeError: Cannot read property 'title' of undefined
        at Proxy.render (/Users/ozansozuoz/Downloads/vue-notebook/src/components/Note.vue:199:832)
        at VueComponent.Vue._render (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:3538:22)
        at VueComponent.updateComponent (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4054:21)
        at Watcher.get (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4465:25)
        at new Watcher (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4454:12)
        at mountComponent (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4061:3)
        at VueComponent.Object.<anonymous>.Vue.$mount (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:8392:10)
        at init (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:3112:13)
        at createComponent (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:5958:9)
        at createElm (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:5905:9)
        at VueComponent.patch [as __patch__] (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:6455:7)
        at VueComponent.Vue._update (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:3933:19)
        at VueComponent.updateComponent (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4054:10)
        at Watcher.get (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4465:25)
        at new Watcher (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4454:12)
        at mountComponent (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:4061:3)
        at VueComponent.Object.<anonymous>.Vue.$mount (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/vue/dist/vue.runtime.common.dev.js:8392:10)
        at mount (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/@vue/test-utils/dist/vue-test-utils.js:13977:21)
        at Object.<anonymous> (/Users/ozansozuoz/Downloads/vue-notebook/tests/unit/example.spec.js:24:25)
        at Object.asyncJestTest (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:102:37)
        at /Users/ozansozuoz/Downloads/vue-notebook/node_modules/jest-jasmine2/build/queueRunner.js:43:12
        at new Promise (<anonymous>)
        at mapper (/Users/ozansozuoz/Downloads/vue-notebook/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
        at /Users/ozansozuoz/Downloads/vue-notebook/node_modules/jest-jasmine2/build/queueRunner.js:73:41
        at processTicksAndRejections (internal/process/task_queues.js:93:5)

 FAIL  tests/unit/example.spec.js
  NoteForm.vue
    ✕ should emit an event when the action v-btn is clicked (501ms)

  ● NoteForm.vue › should emit an event when the action v-btn is clicked

    TypeError: Cannot read property 'title' of undefined

      197 | }
      198 | .read_only_note.theme--dark {
    > 199 |   .v-card__subtitle {
          |                                          ^
      200 |     color: white !important;
      201 |     display: flex;
      202 |     flex-wrap: wrap;

      at Proxy.render (src/components/Note.vue:199:832)
      at VueComponent.Vue._render (node_modules/vue/dist/vue.runtime.common.dev.js:3538:22)
      at VueComponent.updateComponent (node_modules/vue/dist/vue.runtime.common.dev.js:4054:21)
      at Watcher.get (node_modules/vue/dist/vue.runtime.common.dev.js:4465:25)
      at new Watcher (node_modules/vue/dist/vue.runtime.common.dev.js:4454:12)
      at mountComponent (node_modules/vue/dist/vue.runtime.common.dev.js:4061:3)
      at VueComponent.Object.<anonymous>.Vue.$mount (node_modules/vue/dist/vue.runtime.common.dev.js:8392:10)
      at init (node_modules/vue/dist/vue.runtime.common.dev.js:3112:13)
      at createComponent (node_modules/vue/dist/vue.runtime.common.dev.js:5958:9)
      at createElm (node_modules/vue/dist/vue.runtime.common.dev.js:5905:9)
      at VueComponent.patch [as __patch__] (node_modules/vue/dist/vue.runtime.common.dev.js:6455:7)
      at VueComponent.Vue._update (node_modules/vue/dist/vue.runtime.common.dev.js:3933:19)
      at VueComponent.updateComponent (node_modules/vue/dist/vue.runtime.common.dev.js:4054:10)
      at Watcher.get (node_modules/vue/dist/vue.runtime.common.dev.js:4465:25)
      at new Watcher (node_modules/vue/dist/vue.runtime.common.dev.js:4454:12)
      at mountComponent (node_modules/vue/dist/vue.runtime.common.dev.js:4061:3)
      at VueComponent.Object.<anonymous>.Vue.$mount (node_modules/vue/dist/vue.runtime.common.dev.js:8392:10)
      at mount (node_modules/@vue/test-utils/dist/vue-test-utils.js:13977:21)
      at Object.<anonymous> (tests/unit/example.spec.js:24:25)

Я не понимаю, почему он указывает на мой scss и говорит undefined?


person ozansozuoz    schedule 23.02.2021    source источник
comment
Здесь неправильный порядок? импортировать Vue из vue; Vue.use (Vuetify); импортировать Vuetify из vuetify;   -  person Quoc-Anh Nguyen    schedule 23.02.2021
comment
Вы правы, я попытался поместить его ниже импорта vuetify, но все равно ошибка та же.   -  person ozansozuoz    schedule 23.02.2021


Ответы (1)


Вы не добавляете реквизиты в свою оболочку в тестах.

Добавьте этот код в свой тест.

const noteWrapper = mount(Note, {
  localVue,
  vuetify,
  propsData: {
   note: {
    title: '',
  }
});
person Max    schedule 02.06.2021