После написания последней статьи я вернулся к коду и почувствовал, что этот маленький компонент может предложить больше. Текст был уже слишком длинным, поэтому я закончил не так, как намеревался.
Имея это в виду, я решил расширить его и создать серию из четырех частей, в которой я смогу пройти этапы создания этого компонента во всей его красе.
ТОС
- Часть 1: Базовый компонент
- Часть 2. Удаление, фильтрация и передача реквизитов
- Часть 3: Рефакторинг нашего компонента
- Часть 4: Подключение к серверной части и тестирование запросов Ajax
Кстати, есть несколько вещей, которые требуют рефакторинга кода (save()
метод, соглашения об именах,…). Я решил сделать это в конце серии статей, чтобы провести рефакторинг всего компонента. Так что, пожалуйста, оставайтесь со мной до тех пор!
Добавление дополнительных функций
До этого момента у Annotate была возможность: создавать новую заметку; Просмотреть все заметки; и отредактируйте существующую заметку. Хотя это базовая функциональность, я хочу пойти немного дальше и добавить три новые функции:
- Возможность удалить заметку
- Возможность передавать заметки как реквизит
- Возможность фильтрации с поисковым вводом
Итак, приступим!
Уничтожает существующую заметку
Это очень важная функция, которую я не реализовал в первой статье. Чтобы проверить это, давайте подумаем, что нам нужно:
- Учитывая, что у нас есть заметки
- Учитывая, что редактируем первую заметку
- Мы ожидаем увидеть все заметки в списке
- Когда мы нажимаем кнопку «Удалить»
- Ожидаем, что первая заметка не отображается
Исходя из этого, напишем наш тест:
/** @test */ Unite.test("It destroys an existing note", () => { /** Given we have some notes */ addNote("Test note 1", "Note body"); addNote("Test note 2", "Note body"); /** Given we are editing the first note */ wrapper.find(".note:first-child").trigger("click"); /** We expect to see all the notes on the list */ let notes = wrapper.find(".notes"); expect(notes.vnode.children.length).toEqual(2); /** When we click on a "delete" button */ wrapper.find(".destroy").trigger("click"); /** We expect that the first note is not displayed */ let notes = wrapper.find(".notes"); expect(notes.vnode.children.length).toEqual(1); expect(notes.html()).toContain("Test note 2"); expect(notes.html()).not.toContain("Test note 1"); });
Итак, давайте проведем наши тесты!
npm run test
// Error 1) annotate.tests.vue @ It destroys an existing note Error: [vue-test-utils]: find did not return .destroy, cannot call trigger() on empty Wrapper
Не удалось найти элемент «.destroy». Добавим:
// ./src/annotate.tests.vue <template> <div class="annotate"> <div class="new-note" @click="create">Add new note</div> <ul class="notes"> <li class="note" v-for="noteItem in notes" :key="noteItem.id" v-text="noteItem.title" @click="edit(noteItem)"></li> </ul> <div class="form"> <input type="hidden" name="id" v-model="note.id"> <input type="text" name="title" v-model="note.title"> <textarea name="body" v-model="note.body"></textarea> <button @click="save">Save</button> <div class="destroy">Delete</div> </div> </div> </template>
Так что давай снова запустим!
2. npm run test
// Error 1) annotate.tests.vue @ It destroys an existing note expect(received).toEqual(expected) Expected value to equal: 1 Received: 2
Теперь при нажатии на «.destroy» ничего не происходит. Назначение метода destroy()
при нажатии кнопки:
// ./src/annotate.tests.vue <template> <div class="annotate"> <div class="new-note" @click="create">Add new note</div> <ul class="notes"> <li class="note" v-for="noteItem in notes" :key="noteItem.id" v-text="noteItem.title" @click="edit(noteItem)"></li> </ul> <div class="form"> <input type="hidden" name="id" v-model="note.id"> <input type="text" name="title" v-model="note.title"> <textarea name="body" v-model="note.body"></textarea> <button @click="save">Save</button> <div class="destroy" @click="destroy">Delete</div> </div> </div> </template> <script> export default { // Rest of the component... methods: { // Other methods... destroy() { this.notes.forEach((note, key) => { if(note.id === this.note.id) { this.notes.splice(key, 1); } }); } } } </script>
Или, если хочешь быть крутым:
destroy() { this.notes.forEach( (note, key) => (note.id === this.note.id) ? this.notes.splice(key, 1) : "" ); }
Запускаем npm run test
, получаем зелёный!
Он принимает заметки как реквизит
При работе над полным приложением нам нужен способ передачи данных из серверной части в наш компонент при загрузке страницы. Для этого мы можем либо сделать ajax-запрос к API, либо просто передать данные как объект JSON через опору. Реализуем последнее!
Давайте подумаем о нашем тесте:
- Учитывая, что у нас есть массив заметок-объектов
- Если массив передается как опора
- Мы ожидаем увидеть отображаемые заметки
Напишем тест:
/** @test */ Unite.test("It accepts notes as props", () => { /** Given we have an array of note-objects */ let data = [ {id: 1, title: "Note 1", body: "Note body" }, {id: 2, title: "Note 2", body: "Note body" } ]; /** If the array is passed as a prop */ wrapper = Unite.$mount($component, { propsData: { data } }); /** We expect to see the notes displayed */ let notes = wrapper.find(".notes"); expect(notes.vnode.children.length).toEqual(2); expect(notes.html()).toContain("Note 1"); });
Этот тест утверждает, что заметки можно передавать через опору data
. Это означает, что мы сделаем что-то вроде:
<annotate :data="{{ $notes }}"></annotate>
Мы можем изменить это позже на этапе рефакторинга. Другая проблема заключается в том, что для тестирования свойств нам необходимо смонтировать компонент, передавая данные о свойствах через второй аргумент. Из-за этого, поскольку мы монтируем компонент перед каждым тестом без подпорок, мы монтируем его дважды. Это не идеально, и мы немного позже проведем рефакторинг тестов, чтобы этого избежать.
Но давайте сначала заставим его работать!
npm run test
// Error 1) annotate.tests.vue @ It accepts notes as props expect(received).toEqual(expected) Expected value to equal: 2 Received: 0
Итак, мы ожидали увидеть заметки, но ничего не увидели. Чтобы решить эту проблему, давайте сначала определим опору:
// ./src/annotate.tests.vue <script> export default { props: { data: { default() { return []; } } }, // Rest of component... } </script>
Теперь нам нужно назначить данные реквизита для заметок. Для этого подключимся к событию жизненного цикла компонентов created
:
// ./src/annotate.tests.vue <script> export default { props: { data: { default() { return []; } } }, created() { this.notes = this.data; }, // Rest of component... } </script>
Теперь, если мы запустим npm run test
, мы получим зеленый цвет! Потрясающий!
Он фильтрует существующие заметки
В заключение этой статьи давайте реализуем кое-что более интересное: систему фильтрации для нашего списка. Итак, давайте подумаем, как это будет работать:
- Учитывая, что у нас есть заметки
- Если вход фильтра пуст, мы должны увидеть все примечания
- Когда мы вводим во вход фильтра
- Мы ожидаем, что будут отображаться только заметки, содержащие строку.
Достаточно просто! Давайте творим чудеса:
/** @test */ Unite.test("It filters the existing notes", () => { /** Given we have some notes */ addNote("About TDD", "It is awesome!"); addNote("Grocery list", "Melons, apples, cake"); addNote("Party list", "Invitations, balloons, cake"); /** If the filter input is empty, we should see all the notes */ let filter = wrapper.find("input[name=filter]"); expect(filter.element.value).toEqual(""); expect(wrapper.find(".notes").vnode.children.length).toEqual(3); /** When we type into the filter input */ /** We expect to see only notes which include the string */ type("input[name=filter]", "cake"); expect(wrapper.find(".notes").vnode.children.length).toEqual(2); type("input[name=filter]", "TDD"); expect(wrapper.find(".notes").vnode.children.length).toEqual(1); type("input[name=filter]", "RANDOM"); expect(wrapper.find(".notes").vnode.children.length).toEqual(0); });
Давайте проверим наш путь к успеху!
npm run test
// Error 1) annotate.tests.vue @ It filters the existing notes TypeError: Cannot read property 'value' of undefined
На данный момент vue-test-utils не может найти элемент input[name=filter]
. Создадим его:
// ./src/annotate.tests.vue <template> <div class="annotate"> <div class="new-note" @click="create">Add new note</div> <input type="text" name="filter" /> <ul class="notes"> <li class="note" v-for="noteItem in notes" :key="noteItem.id" v-text="noteItem.title" @click="edit(noteItem)"></li> </ul> <div class="form"> <input type="hidden" name="id" v-model="note.id"> <input type="text" name="title" v-model="note.title"> <textarea name="body" v-model="note.body"></textarea> <button @click="save">Save</button> <div class="destroy">Delete</div> </div> </div> </template>
2. npm run test
// Error 1) annotate.tests.vue @ It filters the existing notes expect(received).toEqual(expected) Expected value to equal: 2 Received: 3
Как и ожидалось, хотя мы печатаем, ничего не происходит. Давайте добавим к входным данным директиву v-model
:
// ./src/annotate.tests.vue <template> <div class="annotate"> <div class="new-note" @click="create">Add new note</div> <input type="text" name="filter" v-model="filter" /> <ul class="notes"> <li class="note" v-for="noteItem in notes" :key="noteItem.id" v-text="noteItem.title" @click="edit(noteItem)"></li> </ul> <div class="form"> <input type="hidden" name="id" v-model="note.id"> <input type="text" name="title" v-model="note.title"> <textarea name="body" v-model="note.body"></textarea> <button @click="save">Save</button> <div class="destroy">Delete</div> </div> </div> </template> <script> export default { data() { return { // Other data... filter: "" }; }, // Rest of component... } </script>
Теперь нам нужен способ фильтрации заметок. Итак, давайте воспользуемся вычисляемым свойством для этого:
// ./src/annotate.tests.vue <script> export default { computed: { filtered() { return this.notes.filter(note => { return ( note.title.includes(this.filter) || note.body.includes(this.filter) ); }); } }, // Rest of component... } </script>
Здесь мы просто используем метод filter()
из класса массива для фильтрации результатов, которые включают в себя то, что мы ввели в input[name=filter]
в его заголовке или в теле.
Теперь нам нужно изменить наш цикл v-for
для просмотра отфильтрованного списка:
// ./src/annotate.tests.vue <template> <div class="annotate"> <div class="new-note" @click="create">Add new note</div> <input type="text" name="filter" v-model="filter" /> <ul class="notes"> <li class="note" v-for="noteItem in filtered" :key="noteItem.id" v-text="noteItem.title" @click="edit(noteItem)"></li> </ul> <div class="form"> <input type="hidden" name="id" v-model="note.id"> <input type="text" name="title" v-model="note.title"> <textarea name="body" v-model="note.body"></textarea> <button @click="save">Save</button> <div class="destroy">Delete</div> </div> </div> </template>
Все готово! Если мы запустим npm run test
, мы получим зеленый цвет!
Заключительный компонент
Выводы
В этом примере мы протестировали наш способ реализации трех новых функций для нашего компонента Annotate. Теперь мы можем:
- Создавать заметки
- Просмотр заметок
- Редактировать заметки
- Удалить заметки
- Передавать заметки как реквизит
- Фильтровать заметки
На данный момент наш компонент обладает всеми необходимыми базовыми функциями.
Теперь, когда у нас есть тесты, дающие нам уверенность в необходимости изменения кода, в следующей статье давайте проведем рефакторинг нашего компонента, чтобы сделать его более согласованным и читаемым.
Быть в курсе! знак равно
Итак, поделитесь своими мыслями, вопросами и идеями ниже!
Если вам понравилось, покажите свою поддержку и расскажите своим друзьям!
И спасибо, что прочитали!