preface

This series is about developing component libraries with TDD and trying to bring TDD to the front end. This series is a development diary that covers my thoughts and implementation of TDD while developing components. I won’t go into details about what TDD is or how to use it. If you need to know what TDD is, check out Test-Driven Development. The components to be developed all refer to elementUI, without any development plan, and are written as they please. Ps carefully into the pit, this article is very long

Component description

ElementUI detailed documentation portal about this component

Demand analysis

The first thing to do is to analyze the requirements and see what the component does. List the features

List

  1. You can set the title of the popover
  2. You can set the contents of the popover
  3. You can set whether the popover displays the close button
  4. After clicking the close button, you can close the popover
  5. You can set the callback function after closing
  6. Components can be displayed through function calls

All right, so for the time being, our requirements for the first phase are the ones above.

This list of requirements will continue to grow. It is a TDD trick to start with simple features, and as the requirements are implemented we will gain a deeper understanding of the features, but we do not need to consider all the scenarios at first.

Function implementation

Based on the above function list to implement the corresponding function one by one

You can set the title of the popover

This requirement should be the simplest, so let’s start with the simplest requirement. Let’s write the first test

test

Describe ("Notification", () => {it(" wp-button div", () => {const wrapper = shallowMount(Notification); const result = wrapper.contains(".wp-notification"); expect(result).toBe(true); }); });Copy the code

We first created a div with class of WP-Notification based on the test driver. There were actually questions about whether it was necessary to drive such a small step through testing. My strategy here was to use test driver first and use snapshot function at the end. Then you can delete the test. You can see the logic for writing snapshot tests later.

Write the logic to make it pass the test

Logic implementation

<template>
    <div class="wp-notification">

    </div>
</template>
Copy the code

Then write the second test, which is really setting the title that the popover displays

test

  describe("props".() = > {
    it("Title - you can set the title by title".() = > {
      const wrapper = shallowMount(Notification, {
        propsData: {
          title: "test"}});const titleContainer = wrapper.find(".wp-notification__title");
      expect(titleContainer.text()).toBe("test");
    });
  });
Copy the code

First we control the displayed title by setting the property title, and then we assert that there is a div called.wp-notification__title whose text content is equal to the value we pass in through the property.

Write the logic to make it pass the test

Logic implementation

<template>
    <div class="wp-notification">
        <div class="wp-notification__title">
            {{title}}
        </div>
    </div>
</template>
Copy the code
export default {
    props: {title: {type:String.default:""}}}Copy the code

Okay, we’re going to do the same thing with the content, the close button, and the drive. I’m just going to post the code

Set what the popover displays

test

It ("message - can set the caption with message ", () => {const message = "this is a caption "; const wrapper = shallowMount(Notification, { propsData: { message } }); const container = wrapper.find(".wp-notification__message"); expect(container.text()).toBe(message); });Copy the code

Logic implementation

    <div class="wp-notification__message">
      {{ message }}
    </div
Copy the code
 props: {
    title: {
      type: String.default: ' '
    },
    message: {
      type: String.default: ' '
    },
    showClose: {
      type: Boolean.default: true}},Copy the code

ShowClose – You can set whether the close button is displayed in the popover

test

It ("showClose - control display button ", () => {// Display button by default const Wrapper = shallowMount(Notification); const btnSelector = ".wp-notification__close-button"; expect(wrapper.contains(btnSelector)).toBe(true); wrapper.setProps({ showClose: false }); expect(wrapper.contains(btnSelector)).toBe(false); });Copy the code

Logic implementation

 <button
      v-if="showClose"
      class="wp-notification__close-button"
    ></button>
Copy the code
 props: {
    title: {
      type: String.default: ' '
    },
    message: {
      type: String.default: ' '
    },
    showClose: {
      type: Boolean.default: true}},Copy the code

You can set the callback function after closing

test

Const onClose => {const onClose = jest.fn(); const btnSelector = ".wp-notification__close-button"; const wrapper = shallowMount(Notification, { propsData: { onClose } }); wrapper.find(btnSelector).trigger("click"); expect(onClose).toBeCalledTimes(1); });Copy the code

What we expect is that when the close button is clicked, the onClose function that was passed in is called

Logic implementation

    <button
      v-if="showClose"
      class="wp-notification__close-button"
      @click="onCloseHandler"
    ></button>
Copy the code

Let’s start by adding a click to the button

  props: {
    onClose: {
      type: Function.default: () = >{}}},Copy the code

Add an onClose and call it after clicking the close button.

  methods: {
    onCloseHandler() {
        this.onClose(); }}Copy the code

Components can be displayed through function calls

test

It ("notify() will add notification to body ", () => {notify(); const body = document.querySelector("body"); expect(body.querySelector(".wp-notification")).toBeTruthy(); })Copy the code

We detect whether the notification can be found inside the body as a judgment condition.

Ps: JEST has jSDOM built in, so you can use the Document and other browser apis when testing

Logic implementation

Create a new index.js file

// notification/index.js
import Notification from "./Notification.vue";
import Vue from "vue";
export function notify() {
  const NotificationClass = Vue.extend(Notification);
  const container = document.createElement("div");
  document.querySelector("body").appendChild(container);
  return new NotificationClass({
    el: container
  });
}

window.test = notify;

Copy the code

At this point, we found a problem. The Notification we created was not created through vue-test-utils. There was no way to quickly verify the result of the component by creating a wrapper through the mound as above. We need to find a way to quickly validate the notification component created by notify() again with vue-test-utils.

While looking at Vue-test-utils I found a method: createWrapper(), which we use to create the wrapper object.

Let’s write a test to test this: set the title of the component with notify

test

    it("Set the title".() = > {
      const notification = notify({ title: "test" });
      const wrapper = createWrapper(notification);
      const titleContainer = wrapper.find(".wp-notification__title");
      expect(titleContainer.text()).toBe("test");
    });
Copy the code

We create the Wrapper object with createWrapper and then test the result just as we tested the title before

Logic implementation

import Notification from "./Notification.vue";
import Vue from "vue";
export function notify(options = {}) {
  const container = document.createElement("div");
  document.querySelector("body").appendChild(container);
  return createNotification(container, options);
}

function createNotification(el, options) {
  const NotificationClass = Vue.extend(Notification);

  const notification = new NotificationClass({ el });

  notification.title = options.title;


  return notification;
}
Copy the code

refactoring

Note that I’ve wrapped the logic of the vUE component I created into the createNotification (refactoring is required as the logic increases to keep the code readable, the last step in TDD is refactoring)

Here I hardcoded the value of options.title to notification.title.

Another method is to dynamically assign all the attributes passed through the object.assign () method, but the disadvantage is that the code is very poor reading, when I need to check where the title attribute is assigned, the search code cannot find it at all. So I’m going to give up this dynamic notation here.

So far, we’ve passed the test.

One of the questions is, we already have tests to make sure that the logic for setting the title is correct, so do we need to rewrite this again? The answer I gave is needed here, because notify() is also an API that is leaked to the user, and we need to verify that the result is correct. However, if we don’t need to pass values via the component’s props, then we can delete the previous test. We need to ensure that the tests are unique and cannot be repeated. This means that test driven tests are also allowed to be removed.

Let’s continue to complete the tests and implementation of Message showClose

Set message through notify

test

It (" set message", () => {const message = "this is a message"; const wrapper = wrapNotify({ message }); const titleContainer = wrapper.find(".wp-notification__message"); expect(titleContainer.text()).toBe(message); });Copy the code

refactoring

WrapNotify () : We will find that we need to call createWrapper() to create the corresponding wrapper object each time, instead of wrapping a function directly to facilitate subsequent calls.

    function wrapNotify(options) {
      const notification = notify(options);
      return createWrapper(notification);
    }
Copy the code

This is essentially refactoring, and every time we finish writing tests and logic, we need to stop and see if we need to refactor. Note that test code is also maintainable, so we want to keep the code readable, maintainable, and so on.

Logic implementation


function createNotification(el, options) {
  const NotificationClass = Vue.extend(Notification);
  const notification = new NotificationClass({ el });

  notification.title = options.title;
  notification.message = options.message;

  return notification;
}
Copy the code

Run the notify command to set showClose

test

It (" set showClose", () => {const wrapper = wrapNotify({showClose: false}); const btnSelector = ".wp-notification__close-button"; expect(wrapper.contains(btnSelector)).toBe(false); });Copy the code

Logic implementation


function createNotification(el, options) {
  const NotificationClass = Vue.extend(Notification);
  const notification = new NotificationClass({ el });

  notification.title = options.title;
  notification.message = options.message;
  notification.showClose = options.showClose;

  return notification;
}

Copy the code

refactoring

Ok, this is where we need to stop and see if we need to refactor.

I think the test part of the code is fine for now, can not need to refactor, but let’s look at the business code

// createNotification() function notification.title = options.title; notification.message = options.message; notification.showClose = options.showClose;Copy the code

We wrote them one by one for readability, but as the requirements logic expanded, it started to taste bad. We need to refactor it

function createNotification(el, options) {
  const NotificationClass = Vue.extend(Notification);
  const notification = new NotificationClass({ el });
  updateProps(notification, options);
  return notification;
}

function updateProps(notification, options) {
  setProp("title", options.title);
  setProp("message", options.message);
  setProp("showClose", options.showClose);
}

function setProp(notification, key, val) {
  notification[key] = val;
}
Copy the code
  1. We create a setProp and explicitly write that this action updates the prop. This is where code readability comes in.
  2. Let’s put all the actions that set the properties inside the updateProps to keep the responsibility single

At this point we need to run down one side to see if the refactoring breaks the logic (this is important!).

We took a closer look at the code and found another problem: why do we need to update properties in createNotification(), which is a violation of the single duty?

// index.js
export function notify(options = {}) {
  const container = document.createElement("div");
  document.querySelector("body").appendChild(container);

  const notification = createNotification(container, options);
  updateProps(notification, options);
  return notification;
}

function createNotification(el) {
  const NotificationClass = Vue.extend(Notification);
  const notification = new NotificationClass({ el });
  return notification;
}
Copy the code

So in our refactoring code, we’ve mentioned updateProps() inside notify(), and createNotification() is only responsible for creating the component.

Run tests (important!)

Now let’s see if there’s anything left to refactor.

  const container = document.createElement("div");
  document.querySelector("body").appendChild(container);
Copy the code

Well, once again, we see that this actually fits into a function.

function createContainerAndAppendToView() {
  const container = document.createElement("div");
  document.querySelector("body").appendChild(container);
  return container;
}
Copy the code

Well, that way we know exactly what the function does by its name.

Let’s look at the refactoring notify()

export function notify(options = {}) {
  const container = createContainerAndAppendToView();
  const notification = createNotification(container);
  updateProps(notification, options);
  return notification;
}
Copy the code

Run down the test (important!)

Okay, so the structure looks good for now. We can happily continue to write down the requirements

Set the callback function when onClose –> is closed with notify

test

Const onClose = jest.fn(); const onClose = jest.fn(); const wrapper = wrapNotify({ onClose }); const btnSelector = ".wp-notification__close-button"; wrapper.find(btnSelector).trigger("click"); expect(onClose).toBeCalledTimes(1); });Copy the code

Logic implementation

BTN cannot be found, why?? Think about it

The first thing we need to think about is what will affect the BTN. There is only one point of influence, and that is the options.showClose property. If it is false, the button will not be displayed. In notification. vue, the default value for showClose is true. The problem is that the options we passed to notify must not be showClose when we assigned setProp. So we need to give options a default value.

export function notify(options = {}) {
  const container = createContainerAndAppendToView();
  const notification = createNotification(container);
  updateProps(notification, mergeOptions(options));
  return notification;
}

function mergeOptions(options) {
  return Object.assign({}, createDefaultOptions(), options);
}

function createDefaultOptions() {
  return {
    showClose: true
  };
}
Copy the code

MergeOptions () and createDefaultOptions() are added. This is why we use createDefaultOptions to generate objects instead of using const to directly define a configuration object in the outermost layer. First of all, we know that const does not prevent modification of property values inside an object. A new object is created each time in order to ensure that it is immutable.

Ok, after completing the above logic, the test should just complain that onClose is missing and call it.

function updateProps(notification, options) {
  setProp(notification, "title", options.title);
  setProp(notification, "message", options.message);
  setProp(notification, "showClose", options.showClose);
  / / new
  setProp(notification, "onClose", options.onClose);
}
Copy the code

The fixation

Developing components with TDD — Notification (2)

github

Warehouse code portal

And finally, I want a star


  • This is our team’s open source project, Element3
  • A front end component library that supports VUE3