После написания последней статьи я вернулся к коду и почувствовал, что этот маленький компонент может предложить больше. Текст был уже слишком длинным, поэтому я закончил не так, как намеревался.

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

ТОС

  1. Часть 1: Базовый компонент
  2. Часть 2. Удаление, фильтрация и передача реквизитов
  3. Часть 3: Рефакторинг нашего компонента
  4. Часть 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");
});

Итак, давайте проведем наши тесты!

  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>

Мы можем изменить это позже на этапе рефакторинга. Другая проблема заключается в том, что для тестирования свойств нам необходимо смонтировать компонент, передавая данные о свойствах через второй аргумент. Из-за этого, поскольку мы монтируем компонент перед каждым тестом без подпорок, мы монтируем его дважды. Это не идеально, и мы немного позже проведем рефакторинг тестов, чтобы этого избежать.

Но давайте сначала заставим его работать!

  1. 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);
});

Давайте проверим наш путь к успеху!

  1. 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. Теперь мы можем:

  • Создавать заметки
  • Просмотр заметок
  • Редактировать заметки
  • Удалить заметки
  • Передавать заметки как реквизит
  • Фильтровать заметки

На данный момент наш компонент обладает всеми необходимыми базовыми функциями.

Теперь, когда у нас есть тесты, дающие нам уверенность в необходимости изменения кода, в следующей статье давайте проведем рефакторинг нашего компонента, чтобы сделать его более согласованным и читаемым.

Быть в курсе! знак равно

Итак, поделитесь своими мыслями, вопросами и идеями ниже!

Если вам понравилось, покажите свою поддержку и расскажите своим друзьям!

И спасибо, что прочитали!