vue.js - 使用 Vuetify 进行 Vue 测试。无法读取未定义的属性“标题”
问题描述
我正在尝试测试使用 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 并说未定义?
解决方案
您不会在测试中向包装器添加道具。
将此代码添加到您的测试中。
const noteWrapper = mount(Note, {
localVue,
vuetify,
propsData: {
note: {
title: '',
}
});
推荐阅读
- spring - 未为 PasswordCompareConfigurer 类型定义 contextSource()?
- ios - 如何为 Google 登录委托功能进行单元测试?
- python - Sympy 潜艇没有价值
- ruby-on-rails - Ruby on rails Validation 不适用于包含 Action 文本等的 Form 对象
- python - 使用 Python 库地址标准化地址
- elasticsearch - Elasticsearch 突出显示的术语各不相同
- java - 从 Android (java) 到 Spring Boot 的日期格式
- shopify - 液体编码(Shopify)“and”和“elsif”不能一起工作
- react-native - 如何在 React Native 应用程序的 Cognito 用户池中搜索用户
- c# - 如何添加鼠标灵敏度滑块?