preface

In order to worry about some partners too long to read, I divided into two parts (I don’t know if I can finish the next part).

If you’ve really followed through the code, you’ll notice that the logic is pure JS, and there’s no CSS yet. I haven’t even written CSS or refreshed my browser yet. The tests tell you if the logic is correct (which is why TDD increases development efficiency). Of course, when all the JS logic is done, we need to write style in a little bit, adjust the corresponding HTML structure. The style part is not worth testing.

Demand analysis

As in the previous article, let’s first list the remaining requirements. I actually copied it directly from the elementUI API.

list

  1. OnClick The callback function when you click on Notification
  2. Duration Indicates the time, in milliseconds. If this parameter is set to 0, it will not be automatically closed
  3. Display position

These are the core requirements for this component. Leave the rest of the requirements to you!

Function implementation

OnClick The callback function when you click on Notification

This is easy to test

test

Const onClick = jest.fn(); const onClick = jest.fn(); const wrapper = wrapNotify({ onClick }); const selector = ".wp-notification"; wrapper.find(selector).trigger("click"); expect(onClick).toBeCalledTimes(1); });Copy the code

Code implementation

// Notification.vue <template> <div class="wp-notification" @click="onClickHandler"> <div Class ="wp-notification__title"> {{title}} </div>...... Export default {props: {onClick: {type: Function, default: () => {}}}...... methods: { onClickHandler() { this.onClick(); }}Copy the code
// index.js

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

Run NPM run test:unit

[Vue warn]: Error in v-on handler: "TypeError: this.onClick is not a function"
Copy the code

After we ran NPM Run test:unit, Vue complained. Let’s think about why

Well, if we pass in the notify() parameter, the default value of the component’s props is broken. So we also need to add default values for defaultOptions

// index.js

function createDefaultOptions() {
  return {
    showClose: true,
    onClick: () => {},
    onClose: () => {}
  };
}
Copy the code

refactoring

You can see that inside the updateProps() function, there are so many repetitions that we need to refactor! Kill refactoring so we can win -__-

function updateProps(notification, options) {
  const props = ["title", "message", "showClose", "onClose", "onClick"];
  props.forEach(key => {
    setProp(notification, key, options[key]);
  });
}
Copy the code

We just need to parameter the props.

Run testing after refactoring (important!)

We need to define the default values for options in createDefaultOptions(). We also need to define the default values in notification.vue. Let’s see how we can just use the default values defined in notification. vue

function updateProps(notification, options) { const props = ["title", "message", "showClose", "onClose", "onClick"]; props.forEach(key => { const hasKey = key in options; if (hasKey) { setProp(notification, key, options[key]); }}); }Copy the code

Again, in updateProps(), if we find that the key we are working with does not exist in options, we will not set it so that we do not break the default value that was originally set in notification.vue.

So we set the default options logic is useless, delete!

// Delete all of them
function mergeOptions();
function createDefaultOptions();
Copy the code

Here’s another thing. I’ve seen a lot of projects where the programmer commented out some code that was no longer useful, rather than deleting it. This would have a big impact on the readability of the following code, and the programmer didn’t know why you commented it and whether you could delete it. It is now 9102 years old. If you want to retrieve the original code, you can just retrieve it from Git. Don’t git? I am not responsible for

Take a look at the code again and find it still neat. Ok, let’s continue

Don’t forget to run the test after refactoring!!

Duration Display time: milliseconds. If this parameter is set to 0, it will not be automatically closed

Well, this requirement can be split into two tests

  1. When the value is greater than 0, the system automatically shuts down when the time is up
  2. When the value is 0, it will not be automatically closed

When the time is greater than 0 seconds, the system automatically shuts down

test
jest.useFakeTimers(); ... Describe ("duration display time ", () => {it(" > 0, automatic close ", () => {const duration = 1000; wrapNotify({ duration }); const body = document.querySelector("body"); expect(body.querySelector(".wp-notification")).toBeTruthy(); jest.runAllTimers(); expect(body.querySelector(".wp-notification")).toBeFalsy(); }); });Copy the code

Here we need to mock the time with Jest, because unit tests are meant to be fast and we can’t wait for a real delay.

For jest based documentation, use jest.usefaketimers () and then jest.runalltimers (); To get setTimeout to fire quickly. The validation component exists before firing and does not exist after firing.

Implementation logic
// index.js function updateProps(notification, options) {const props = [... "duration"]; setDuration(notification.duration, notification); } function setDuration(duration, notification) { setTimeout(() => { const parent = notification.$el.parentNode; if (parent) { parent.removeChild(notification.$el); } notification.$destroy() }, options.duration); }Copy the code
// notification. vue props: {...... duration: { type:Number, default: 4500 } }Copy the code

The same logic for adding attributes as before, except for the special handling of the duration logic. We use setTimeout in setDuration() to implement the logic of deferred deletion.

refactoring

When we put on the refactoring hat, a lot of the logic in setTimeout is actually for deletion. So why not extract it as a function?

function setDuration(options, notification) {
  setTimeout(() = > {
    deleteNotification(notification);
  }, options.duration);
}

function deleteNotification(notification) {
  const parent = notification.$el.parentNode;
  if (parent) {
    parent.removeChild(notification.$el);
  }
  notification.$destroy();
}
Copy the code

Run testing after refactoring (important!)

When the value is 0, it will not be automatically closed

test
      it("Equal to zero, does not automatically close.".() = > {
        const duration = 0;
        wrapNotify({ duration });
        const body = document.querySelector("body");
        expect(body.querySelector(".wp-notification")).toBeTruthy();
        jest.runAllTimers();
        expect(body.querySelector(".wp-notification")).toBeTruthy();
      });
Copy the code
Logic implementation

The logical implementation here is simple

// index.js
function setDuration(duration, notification) {
  if (duration === 0) return;
  setTimeout(() = > {
    deleteNotification(notification);
  }, duration);
}
Copy the code
refactoring

Once the logic is implemented, we need to put on the refactoring hat!

As you can see, the above two tests already have a significant overlap

 describe("Duration displays time".() = > {
      it("When greater than 0, automatic shutdown.".() = > {
        const duration = 1000;
        wrapNotify({ duration });
        const body = document.querySelector("body");
        expect(body.querySelector(".wp-notification")).toBeTruthy();
        jest.runAllTimers();
        expect(body.querySelector(".wp-notification")).toBeFalsy();
      });

      it("Equal to zero, does not automatically close.".() = > {
        const duration = 0;
        wrapNotify({ duration });
        const body = document.querySelector("body");
        expect(body.querySelector(".wp-notification")).toBeTruthy();
        jest.runAllTimers();
        expect(body.querySelector(".wp-notification")).toBeTruthy();
      });
    });
Copy the code

We need to extract their repeated logic into a function

Describe ("duration show time ", () => {let body; function handleDuration(duration) { wrapNotify({ duration }); body = document.querySelector("body"); expect(body.querySelector(".wp-notification")).toBeTruthy(); jest.runAllTimers(); } handleDuration(1000) => {handleDuration(1000); expect(body.querySelector(".wp-notification")).toBeFalsy(); }); HandleDuration (0); handleDuration(0); expect(body.querySelector(".wp-notification")).toBeTruthy(); }); });Copy the code

Don’t forget to run the quiz

Popovers that are automatically closed by Duration should call onClose

This requirement point that I just figured out, we only implemented onClose when we hit the close button. But when closed by Duration, onClose should also be called.

test
    it("OnClose is also called when closed by setting duration".() = > {
        const onClose = jest.fn();
        wrapNotify({ onClose, duration: 1000 });
        jest.runAllTimers();
        expect(onClose).toBeCalledTimes(1);
      });
Copy the code
Code implementation
// index.js
function setDuration(duration, notification) {
  if (duration === 0) return;
  setTimeout(() = > {
    // add new logic
    notification.onClose();
    deleteNotification(notification);
  }, duration);
}
Copy the code

We just need to call onClose inside setDuration(). Since we already set its default value to a function (in notification. vue), we don’t need to determine whether onClose exists here either.

Display position

Let’s just deal with the default coordinates, which appear in elementUI on the top right.

There is also logic about how coordinates are managed when multiple Notifications are displayed simultaneously.

test

    describe("Displayed coordinates".() = > {
      it("The default position of the first component displayed is top: 50px, right:10px".() = > {
        const wrapper = wrapNotify();
        expect(wrapper.vm.position).toEqual({
          top: "50px".right: "10px"
        });
      });

Copy the code

Since VUE is the framework of MVVM, we only need to assert position (model => view).

Logic implementation

// index.js export function notify(options = {}) {...... updatePosition(notification); return notification; } function updatePosition(notification) { notification.position = { top: "50px", right: "10px" }; }Copy the code
// notification. vue // add data.position data(){return {position:{top: "", right: <div class="wp-notification" :style="position" @click="onClickHandler">Copy the code

All right, so we can pass the test. But writing it this way does not satisfy our need for multiple component display positions. That’s ok, TDD is what it is. When you can’t think of logic in one breath, you can do it in small steps like this. In TEST Driven Development this approach is called triangulation. We continue to

test

      it("When displaying both components, the position of the second component is top: 125px, right:10px".() = > {
        
        wrapNotify();
        const wrapper2 = wrapNotify();
        expect(wrapper2.vm.position).toEqual({
          top: "125px".right: "10px"
        });
      });
Copy the code

Let’s assume that when displaying two components at the same time, the position of the second component is top: 125px and right:10px, although we know that the correct logic would be: position of the first component + height of the first component + distance between them. But don’t worry, let’s default the height of the component to be fixed. Simple implementation first, then correct logic at the end. This is where the idea of breaking up a task into small, simple tasks and breaking them down.

Code implementation

const notificationList = [];
export function notify(options = {}) {
  ……
  notificationList.push(notification);
  updateProps(notification, options);
  updatePosition(notification);
  return notification;
}

function updatePosition() {
  const interval = 25;
  const initTop = 50;
  const elementHeight = 50;

  notificationList.forEach((element, index) = > {
    const top = initTop + (elementHeight + interval) * index;
    element.position.top = `${top}px`;
    element.position.right = `10px`;
  });
}
Copy the code

How do we handle multiple component displays? Our strategy here is to store all the previously created components in an array, and then process the top value in updatePosition() based on the number of previously created components.

Run the test ~ failed, the following tips

The first component position to be displayed defaults to top: 50px, right:10px expect(received).toEqual(expected) // deep equality - Expected + Received Object { "right": "10px", - "top": "50px", + "top": "725px", }Copy the code
When two components are displayed at the same time, the position of the second component is top: 125px, right:10px expect(received).toEqual(expected) // deep equality - Expected + Received Object { "right": "10px", - "top": "125px", + "top": "875px", }Copy the code

Both tests failed. One top was 725px and the other 875px. Why??

There can only be one reason for this. The notificationList is never zero in length when we test the display coordinates.

Because its scope is global. The components created in our previous tests were added to the array. But it didn’t delete it. So its length is not zero in the above test. Well, we’ve found the problem that caused this, and we’ve solved more than half of it by finding out what the problem is. Then we just need to clear the notificationList before each test run.

Then again, how do we clear it? Because it is esmoudle, we do not export the notificationList, so there is no way to assign its length to zero in the test class. So do we need to export this array? Does not make sense ah, export breaks the encapsulation ah, how to do?

There is a babel-plugin-rewire plug-in to solve this problem

Let’s deal with the test logic according to the document

The introduction of rewire

npm install babel-core babel-plugin-rewire
Copy the code
// babel.config.js module.exports = {presets: ["@vue/cli-plugin-babel/preset"], // New plugins: ["rewire"]};Copy the code
// Notification.spec.js import { notify, __RewireAPI__ as Main } from ".. /index"; describe("Notification", () => { beforeEach(() => { Main.__Rewire__("notificationList", []); }); ...Copy the code

First install, then configure the plug-in in babel.config.js, then test the processing logic in the class: beforeEach() hook function, clear out the notificationList, so we remove the dependencies between each test. It is now ready to pass the test

test

  it("After all created components have disappeared, the position of the newly created component should be the starting position.".() = > {
        wrapNotify();
        jest.runAllTimers();
        const wrapper2 = wrapNotify();
        expect(wrapper2.vm.position).toEqual({
          top: "50px".right: "10px"
        });
      });
Copy the code

This test is to see if the location is correct when a new component is created after the first component has disappeared (the correct location should return to the start bit).

After displaying a component through wrapNotify() and triggering the logic to remove the component with jest. RunAllTimers (), we create the component again and check its location

Logic implementation
// Notification.vue...data(){
    return {
      position: {top:"".right:""}}},...computed: {
    styleInfo(){
      return Object.assign({},thisThe position)}},... <divclass="wp-notification" :style="styleInfo" @click="onClickHandler">
Copy the code

We use the computed property here to trigger the style update when position is reassigned.

// index.js

let countId = 0;
function createNotification(el) {
  const NotificationClass = Vue.extend(Notification);
  const notification = new NotificationClass({ el });
  notification.id = countId++;
  return notification;
}

function deleteNotification(notification) {
  const parent = notification.$el.parentNode;
  if (parent) {
    parent.removeChild(notification.$el);
  }
  removeById(notification.id);
  notification.$destroy();
}

function removeById(id) {
  notificationList = notificationList.filter(v= >v.id ! == id); }Copy the code

To satisfy the above tests, we should remove the component from the notificationList when it is deleted. It’s best to delete them when you call deleteNotification(). But we need to know which component we are removing. So we gave it a unique id. The component can be deleted based on the ID.

test

      it("Create two components. When the first component disappears, the location of the second component should be updated -> updated to the location of the first component.".() = > {
        wrapNotify({ duration: 1000 });
        const wrapper2 = wrapNotify({ duration: 3000 });
        jest.advanceTimersByTime(2000);
        expect(wrapper2.vm.position).toEqual({
          top: "50px".right: "10px"
        });
      });
Copy the code

Let me describe the purpose of this test in more detail. If we use the Notification component in elementUI, we should know that when I list multiple Notification components in one go, the one that appears first will disappear, and when it disappears, the next component will go up.

First we create two components and make the first component disappear faster (set to 1 second) and the second component disappear slower (set to 3 seconds). Then we use jest. AdvanceTimersByTime (2000); Let the timer run two seconds fast. The first component should now disappear. Ok, so the test reported an error. As expected, the position of the second component does not change. There is a particularly important point here, and you need to know when your tests should fail and when they should be right. You can’t program by coincidence!

Logic implementation

// index.js function setDuration(duration, notification) { if (duration === 0) return; setTimeout(() => { notification.onClose(); deleteNotification(notification); // Add logic updatePosition(); }, duration); }Copy the code

We just need to call updatePosition() when we delete the component again. This is thanks to the fact that we have packaged each function into a separate function, making it easy to reuse now.

refactoring

So far, we have driven the component’s coordinate logic out. Oh, by the way, we hardcoded the height of the component, that needs to be tweaked, so let’s write that requirement down and put it on the requirements List, and sometimes when we do a requirement we realize we might need to do something else, don’t panic let’s just write down what we need to do later, Wait until we finish what we need right now. Don’t get distracted just yet! So let’s look at where does the code need to be refactored

It looks like there are three repetitions of the initial coordinates in the test file (index.js), which we extracted first

Describe (" display coordinates ", () => {const initPosition = () => {return {top: "50px", right: "10px"}; }; const expectEqualInitPosition = wrapper => { expect(wrapper.vm.position).toEqual(initPosition()); }; It (" The first component to display defaults to top: 50px, right:10px ", () => {const wrapper = wrapNotify(); expectEqualInitPosition(wrapper); }); It (" top: 125px, right:10px", () => {wrapNotify(); const wrapper2 = wrapNotify(); expect(wrapper2.vm.position).toEqual({ top: "125px", right: "10px" }); }); It (" After the first component disappears, the position of the newly created component should be the starting position ", () => {wrapNotify(); jest.runAllTimers(); const wrapper2 = wrapNotify(); expectEqualInitPosition(wrapper2); }); It (" after the first component disappears, the position of the second component should be updated to the position of the first component ", () => {wrapNotify({duration: 1000}); const wrapper2 = wrapNotify({ duration: 3000 }); jest.advanceTimersByTime(2000); expectEqualInitPosition(wrapper2); }); });Copy the code

It looks pretty readable for a while.

// index.js

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

There’s a notificationList.push(notification); Well, I don’t like it very much. I think the readability is a bit poor.

// index.js

export function notify(options = {}) {... addToList(notification); ... }function addToList(notification) {
  notificationList.push(notification);
}
Copy the code

It looks so much better. You can see what you’re doing.

Don’t forget to run the test after refactoring!!

test

Now it’s time to get back to the “component height” requirement. Remember, we used to write the dead height in the program. You now need to fetch dynamically based on the height of the component.

Previous tests

      it("When displaying both components, the position of the second component is top: 125px, right:10px".() = > {
        wrapNotify();
        const wrapper2 = wrapNotify();
        expect(wrapper2.vm.position).toEqual({
          top: "125px".right: "10px"
        });
      });
Copy the code

We need to refactor this test

      it("When two components are displayed at the same time, the position of the second component is -> start position + height of the first component + interval".() = > {
        const wrapper1 = wrapNotify();
        const wrapper2 = wrapNotify();
        const initTop = 50;
        const top = initTop + interval + wrapper1.vm.$el.offsetHeight;
        expect(wrapper2.vm.position).toEqual({
          top: `${top}px`.right: "10px"
        });
      });
Copy the code

The refactored test no longer hardcodes the value of the second component. However, there are still some disadvantages. Remember that the interval values interval and initTop values were defined in index.js

// index.js

function updatePosition() {
  const interval = 25;
  const initTop = 50; ... }Copy the code

There is no need to expose the values of these two variables for now, as above, we use Rewire to solve this problem.

Update our tests

      it("When two components are displayed at the same time, the position of the second component is -> start position + height of the first component + interval".() = > {
        const wrapper1 = wrapNotify();
        const wrapper2 = wrapNotify();
        const interval = Main.__get__("interval");
        const initTop = Main.__get__("initTop");
        const top = initTop + interval + wrapper1.vm.$el.offsetHeight;
        expect(wrapper2.vm.position).toEqual({
          top: `${top}px`.right: "10px"
        });
      });

Copy the code

The interval and initTop that are not exposed in index.js are obtained by Rewire.

Logic implementation

const interval = 25;
const initTop = 50

function updatePosition() {
  notificationList.forEach((element, index) = > {
    const preElement = notificationList[index - 1];
    const preElementHeight = preElement ? preElement.$el.offsetHeight : 0;
    const top = initTop + (preElementHeight + interval) * index;
    element.position.top = `${top}px`;
    element.position.right = `10px`;
  });
}

Copy the code

Bring interval and initTop into global scope.

Calculate the top value of subsequent components based on the formula: starting position + previous component height + interval.

refactoring

Once the logical implementation passes the test, it’s time to refactor. Let’s see where we need to refactor?

Let’s focus first on updatePosition(), which I think is getting less readable internally.

function updatePosition() {
  const createPositionInfo = (element, index) = > {
    const height = element ? element.$el.offsetHeight : 0;
    const top = initTop + (height + interval) * index;
    const right = 10;
    return {
      top: `${top}px`.right: `${right}px`
    };
  };

  notificationList.forEach((element, index) = > {
    const positionInfo = createPositionInfo(element, index);
    element.position.top = positionInfo.top;
    element.position.right = positionInfo.right;
  });
}
Copy the code

We split the logic into a createPositionInfo() function that gets the position data to be updated. So when we read the code again, we can see what it’s doing at a glance. Since createPositionInfo() is closely related to updatePosition(), I chose to make it an inline function, but that doesn’t matter, we can easily extract it if we need to change it in the future.

The problem is that jsDOM does not actually render elements, so when we test the offsetHeight of elements we will always get a 0. How to do? We can mock out the height of the real element and give it a false value.

Modify the tests first

      it("When two components are displayed at the same time, the position of the second component is -> start position + height of the first component + interval".() = > {
        const interval = Main.__get__("interval");
        const initTop = Main.__get__("initTop");
        const elementHeightList = [50.70];
        let index = 0;
        Main.__Rewire__("getHeightByElement".element= > {
          return element ? elementHeightList[index++] : 0;
        });

        wrapNotify();
        const wrapper2 = wrapNotify();
        const top = initTop + interval + elementHeightList[0];
        expect(wrapper2.vm.position).toEqual({
          top: `${top}px`.right: "10px"
        });
      });
Copy the code

Let’s say we have a getHeightByElement() method that returns the height of the element, which is essentially a seam, and mock its behavior for testing purposes.

One important point is that we need to assume that the height of each component is different (if it is the same, it will be the same as the false value we wrote earlier).

Implementation logic

function getHeightByElement(element) {
  return element ? element.$el.offsetHeight : 0;
}

function updatePosition() {
  const createPositionInfo = (element, index) = > {
    constheight = getHeightByElement(element); ...Copy the code

The test should have failed! Why? Looking back at our previous updatePosition() logic, we used the current element directly to get the height of the element. The correct logic would have been to use the height of the previous element. We found the bug through testing! And then modify it

function updatePosition() { const createPositionInfo = (element, index) => { const height = getHeightByElement(element);  const top = initTop + (height + interval) * index; const right = 10; return { top: `${top}px`, right: `${right}px` }; }; ForEach ((element, index) => {// add logic const preElement = notificationList[index-1]; const positionInfo = createPositionInfo(preElement, index); element.position.top = positionInfo.top; element.position.right = positionInfo.right; }); }Copy the code

We get the previous component with notificationList[index-1]. The test should pass smoothly by now!

Don’t forget to run tests after refactoring!!

Clicking the close button requires the component to close

This requirement occurred to me earlier. When we did the close button requirement, we only verified that closing would call onClose, but we didn’t verify that the component was closed. When we think of a requirement that is not being addressed, we should add it to our requirements List and then go back to it when the requirement at hand is completed.

test

      describe("Click the Close button".() = > {
        it("Components should be removed".() = > {
          const wrapper = wrapNotify();
          const body = document.querySelector("body");
          const btnSelector = ".wp-notification__close-button";
          wrapper.find(btnSelector).trigger("click");
          expect(body.querySelector(".wp-notification")).toBeFalsy();
        });
      });
Copy the code

Verify that the component still exists by toBeFalsy()

Logic implementation

// Notification.vue

  methods: {
    onCloseHandler() {
      this.onClose();
    +  this.$emit('close'.this)},Copy the code
// index.js function createNotification(el) { const NotificationClass = Vue.extend(Notification); const notification = new NotificationClass({ el }); + notification.$on("close", onCloseHandler); notification.id = countId++; return notification; } + function onCloseHandler(notification) { + deleteNotification(notification); +}Copy the code

Delete by listening to the component emit a close event

Oh, the test failed, telling us that there is still a Notification component in the body. Why??

There’s a logic we’ve been missing: remember when we did the duration logic? At that time, there was a setTimeout, duration and then the logic to delete the component would be triggered. However, we only created the component in the previous test but did not do the logic to clear it, which led to the failure of our above test.

describe("Notification".() = > 
  + afterEach(() = >{ + jest.runAllTimers(); +})Copy the code

After each test call, jest. RunAllTimers () was called to trigger setTimeout in time. In this way, the components created by each test were deleted successfully after the test.

From the above lessons, we should realize how important the life cycle of a test is. A test must be destroyed after completion, otherwise subsequent tests will fail!!

refactoring

With the test passed, let’s move on to see where we need to refactor

      describe("Click the Close button".() = > {
        it("Call onClose".() = > {
          const onClose = jest.fn();
          const wrapper = wrapNotify({ onClose });
          const btnSelector = ".wp-notification__close-button";
          wrapper.find(btnSelector).trigger("click");
          expect(onClose).toBeCalledTimes(1);
        });

        it("Components should be removed".() = > {
          const wrapper = wrapNotify();
          const body = document.querySelector("body");
          const btnSelector = ".wp-notification__close-button";
          wrapper.find(btnSelector).trigger("click");
          expect(body.querySelector(".wp-notification")).toBeFalsy();
        });
      });
Copy the code

We can find two repetitions:

  1. Gets the logic for the close button
  2. Check for component presence (there are several places where test logic checks for component presence via body)

Gets the logic for the close button

      function clickCloseBtn(wrapper) {
        const btnSelector = ".wp-notification__close-button";
        wrapper.find(btnSelector).trigger("click");
      }
Copy the code

Checks whether the component exists in the view

    function checkIsExistInView() {
      const body = document.querySelector("body");
      return expect(body.querySelector(".wp-notification"));
    }
Copy the code

Next, we replace all the logic that checks if the component exists in the view

// Check if checkIsExistInView().tobetruthy (); // Check if there is no checkIsExistInView().tobefalsy ();Copy the code

Click the close button – onClose will only be called once

The initial test we wrote was to call onClose, but we know from the previous test that the component ends up executing the logic inside setTimeout.

function setDuration(duration, notification) {
  if (duration === 0) return;
  setTimeout(() = > {
    notification.onClose();
    deleteNotification(notification);
    updatePosition();
  }, duration);
}
Copy the code

OnClose () is called once more, and onClose() may be called twice. Let’s change the test again to verify this bug

test

        it("Call onClose".() = > {
          const onClose = jest.fn();
          const wrapper = wrapNotify({ onClose });
          clickCloseBtn(wrapper);
          expect(onClose).toBeCalledTimes(1);
        });
Copy the code

Refactoring is

        it("OnClose will only be called once.".() = > {
          const onClose = jest.fn();
          const wrapper = wrapNotify({ onClose });
          clickCloseBtn(wrapper);
          expect(onClose).toBeCalledTimes(1);
          // After the component is destroyed
          jest.runAllTimers();
          expect(onClose).toBeCalledTimes(1);
        });
Copy the code

Use jest. RunAllTimers () to trigger the logical execution inside setTimeout.

Sure enough, one side was already through.

Logic implementation

function setDuration(duration, notification) {
  if (duration === 0) return;
  setTimeout(() = >{+if (isDeleted(notification)) return;
    notification.onClose();
    deleteNotification(notification);
    updatePosition();
  }, duration);
}
Copy the code

If the component is removed, we simply do not perform the following logic, thus avoiding repeated calls to onClose() when setTimeout () is executed

function isDeleted(notification) { return ! notificationList.some(n => n.id === notification.id); }Copy the code

Our previous logic was that when a component is deleted, it is removed from the notificationList. Therefore, we can check whether there is a corresponding ID in the list.

refactoring

// Notification.vue
  methods: {
    onCloseHandler() {
      this.onClose();
      this.$emit('close'.this)},Copy the code

OnClose () is called when we click the close button again, but I want to tweak it to put the logic that calls onClose() inside index.js.

// Notification.vue

  methods: {
    onCloseHandler() {
      this.$emit('close'.this)},Copy the code
// index.js
function onCloseHandler(notification) {
  notification.onClose();
  deleteNotification(notification);
}
Copy the code

Run the test after reconstruction ~~~

Oh, the test failed: click the close button and the onClose passed in should be called

But let’s think about it, the logic to call the component is through notify(), so I don’t think it’s a problem to remove the test here.

You need to update the coordinates after clicking the close button

This requirement was also suddenly realized when I did the last test. After closing the button, I need to update the coordinates. Now let’s get it out of the list and do it

test

      describe("Create two components. When the first component disappears, the location of the second component should be updated -> updated to the location of the first component.".() = > {
        it("Disappear by clicking the Close button.".() = > {
          const wrapper1 = wrapNotify();
          const wrapper2 = wrapNotify();
          clickCloseBtn(wrapper1);
          expectEqualInitPosition(wrapper2);
        });

        it("Disappear by triggering setTimeout".() = > {
          wrapNotify({ duration: 1000 });
          const wrapper2 = wrapNotify({ duration: 3000 });
          jest.advanceTimersByTime(2000);
          expectEqualInitPosition(wrapper2);
        });
      });
Copy the code

Let’s recall that the tests we wrote previously only validated tests that validated after the component was deleted via setTimeout. Clicking the close button should be the same logic as triggering setTimeout.

Logic implementation

// index.js
function onCloseHandler(notification) {
  notification.onClose();
  deleteNotification(notification);
  + updatePosition();
}
Copy the code

Ok, now all you need to do is click the close button again and call updatePosition() to update the position.

refactoring

function onCloseHandler(notification) {
  notification.onClose();
  deleteNotification(notification);
  updatePosition();
}
Copy the code
function setDuration(duration, notification) {
  if (duration === 0) return;
  setTimeout(() = > {
    if (isDeleted(notification)) return;
    notification.onClose();
    deleteNotification(notification);
    updatePosition();
  }, duration);
}
Copy the code

As you can see, we called notification.onclose (), deleteNotification(), and updatePosition() in both places. In line with the principle of not repeating, we will encapsulate it again

function handleDelete(notification) {
  if (isDeleted(notification)) return;
  notification.onClose();
  deleteNotification(notification);
  updatePosition();
}
Copy the code

Then replace the logic in onCloseHandler and setDuration

function setDuration(duration, notification) {
  if (duration === 0) return;
  setTimeout(() = > {
    handleDelete(notification);
  }, duration);
}
Copy the code
function onCloseHandler(notification) {
  handleDelete(notification);
}
Copy the code

Ps: Sometimes it’s hard to come up with a good name

Don’t forget to run the quiz

The snapshot

With our component core logic basically in place, it’s time to add Snapshot Testing

  it("Snapshot".() = > {
    const wrapper = shallowMount(Notification);
    expect(wrapper).toMatchSnapshot();
  });
Copy the code

Is very simple, need only two lines of code, and then will be __test__ jest/snapshots to generate a file under Notification. Spec. Js. The snap

__test__/__snapshots__/Notification.spec.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[1 ` ` Notification snapshot] = ` 
      
`
; Copy the code

You can see that it stores the current state of the component as a string, which Jest calls a (rather figurative) snapshot.

This will alert us when we change the component and break the snapshot.

Delete unused tests

We need to remove tests when they no longer make sense, and remember that tests, like production code, need to be maintained and iterated

Once we have the snapshot, we can delete the following test

It (" there should be a div for wp-notification ", () => {const wrapper = shallowMount(notification); const result = wrapper.contains(".wp-notification"); expect(result).toBe(true); });Copy the code

Delete tests done directly on components

We call components through notify(), not directly. So our initial testing of the component can also be removed

  describe("props".() = > {
    it("Title - Title can be set by title".() = > {

    it("Message - Text can be set by message".() = > {

    it("ShowClose - Control display button".() = >{});Copy the code

conclusion

Why write this article

When I studied TDD at the beginning, I checked all the materials on the Internet, and there were basically no tutorials for implementing TDD in the front end. Some of them were just a few simple demos, which basically could not meet the daily work scenarios. So I thought about writing another article. The original goal was to write a component library and write out every component, but after writing this article, I found that it was too long to write even one component. It’s almost too long for you to read. And then we’ll think about making a video.

It took me a long time to write this article, and I usually complete one or two small requirements every day. That’s a long time after more than a week. In fact, it is not a tutorial, the basic is my personal development diary, what problems encountered, how to solve. I think the process is more valuable than the end result. So it took more than a week to finish this article. I hope I can help you.

TDD versus traditional methods

The traditional way

In the traditional way of development, we will refresh the browser frequently during the process of development, and then debug the code in Chrome. The basic process is:

Writing code – > refresh the browser – > see view | look at the console. The value of the log out

This process will repeat itself over and over again, as I’m sure you all know

TDD

We’re driven by tests, and we write code just so the tests pass. We break down the overall requirements into small tasks and break them down one by one.

Write tests -> Run tests (RED) -> Write code to pass tests (Green) -> Refactor

In my own sense, the cost is a matter of habit, and the cost of writing the test. There are many people who can barely write tests, so there is the illusion that TDD is difficult to implement. According to my own practice, LEARN how to write tests first, then learn how to refactor. You can basically get started with TDD. With the guarantee of testing, we don’t have to debug again and again, and it’s all automated. To ensure their own code quality, reduce bugs, improve development efficiency. 996 is just around the corner.

Say more, there are so many tests above, although the writing is quite long, in fact, it only takes 5-20 minutes to pass one.

To study the reference

Finally, I recommend some learning links

  1. Vue Strategies and Practices for applying unit Testing 01 – Introduction and Objectives
  2. Is TDD (Test Driven Development) dead? – Answer by Li Xiaobo – Zhihu

Lv Liqing’s vUE unit Test series articles can make you easily start with how to write tests, and then we will cooperate with geek Academy to launch front-end TDD training camp, interested students can pay attention to

github

Warehouse code portal

Finally, find star ~~

The last

Developing Components with TDD — Notification (Part 1)

Afterword.

Later, if I have time, I will share it by recording a video. Some things are difficult to express in words.

Set a flag and bring TDD to the front end


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