Setting up single-player tests before adding spinner

https://jsfiddle.net/q46u1rog/1/

  function stubYT(iframe, spies = {}) {
    const dummyFunc = () => null;
    window.YT = {
      Player: function makePlayer(video, options) {

Paul is really going out of his way to help you here. Please show some common decency and give a little explanation as to what you’ve been doing and why, instead of just dropping a link and some code without any context whatsoever.

3 Likes

That spies object will be getting a lot of use when it comes to the next set of tests.

Before moving on to those though, is there any other refactoring that needs to be done?
Yes there is. The playerReady description has two spaces in a row in the description string, when there should only be one space.

In that area I also notice that some of the line indenting is messed up, so click on Tidy at the top-right of the JavaScript panel to fix that up.

1 Like

I fixed that: https://jsfiddle.net/o7fyg4ub/

describe("videoPlayer tests", function() {
  let player;

  function removeIframeScripts() {
    const scripts = document.querySelectorAll("script");
    scripts.forEach(function removeScript(script) {
      const url = script.getAttribute("src");
      if (url === "https://www.youtube.com/iframe_api") {
        script.remove();
      }
    });
  }

  function createVideo() {
    const video = document.createElement("div");
    video.classList.add("video");
    return video;
  }

  function stubYT(iframe, spies = {}) {
    const dummyFunc = () => null;
    window.YT = {
      Player: function makePlayer(video, options) {
        player = {
          h: iframe,
          i: {
            h: options
          },
          playVideoAt: dummyFunc,
          m: video,
          setShuffle: dummyFunc,
          setVolume: spies.setVolume,
          stopVideo: dummyFunc
        };
        return player;
      }
    };
  }

  function triggerAfterPlayerReady(el) {
    const afterPlayerReadyEvent = new window.CustomEvent("afterPlayerReady");
    el.dispatchEvent(afterPlayerReadyEvent);
  }
  describe("init", function() {
    let iframe;
    beforeEach(function() {
      removeIframeScripts();
      iframe = document.createElement("iframe");
      stubYT(iframe);
    });
    it("makes onYouTubeIframeAPIReady available", function() {
      videoPlayer.init()
      expect(typeof window.onYouTubeIframeAPIReady).toBe("function");
    });
    it("loads iframe script", function() {
      //given
      removeIframeScripts();

      //when
      videoPlayer.init();

      //then
      const src = document.querySelector("script").src;
      expect(src).toBe("https://www.youtube.com/iframe_api");
    });
    it("afterPlayerReady handler", function() {
      //given
      const spy = jasmine.createSpy("afterPlayerReady-handler");
      videoPlayer.init({
        afterPlayerReady: spy
      });
      const video = createVideo();
      videoPlayer.addPlayer(video);

      //when
      triggerAfterPlayerReady(iframe);

      //then
      expect(spy).toHaveBeenCalled();
    });
  });
  describe("addPlayer", function() {
    let iframe;
    let video;
    beforeEach(function() {
      removeIframeScripts();
      iframe = document.createElement("iframe");
      stubYT(iframe);
      video = createVideo();
    });
    it("addPlayer requires a video element", function() {
      //given
      const badVideo = document.createElement("div");

      //then
      function badArgument() {
        videoPlayer.addPlayer(badVideo);
      }
      expect(badArgument).toThrowError(TypeError, /Element needs a video classname/);
    });
    it("passes video to the player object", function() {
      //given
      player = undefined;

      //when
      videoPlayer.addPlayer(video);

      //then
      expect(player.m.classList).toContain("video");
    });

    it("it has dimensions", function() {
      //given
      player = undefined;

      //when
      videoPlayer.addPlayer(video);

      //then
      const options = player.i.h;

      expect(typeof options.width).toBe("number");
      expect(options.width).toBeGreaterThan(0);
    });

    it("it has playerVars", function() {
      //given
      player = undefined;

      //when
      videoPlayer.addPlayer(video);

      //then
      const playerVars = player.i.h.playerVars;

      expect(typeof playerVars.cc_load_policy).toBe("number");
      expect(playerVars.cc_load_policy).toBe(0);
    });

    it("has a playlist", function() {
      //given
      player = undefined;

      //when
      videoPlayer.addPlayer(video);

      //then
      const playerVars = player.i.h.playerVars;
      expect(typeof playerVars.playlist).toBe("string");
    });

    it("has onReady event", function() {
      //given
      player = undefined;

      //when
      videoPlayer.addPlayer(video);

      //then
      const options = player.i.h;
      expect(typeof options.events.onReady).toBe("function");
    });
  });
  describe("playerReady tests", function() {

    let iframe;
    let setVolumeSpy;
    let video;

    function initVideoPlayer() {
      videoPlayer.init();
      removeIframeScripts();
    }

    beforeEach(function() {
      initVideoPlayer();
      iframe = document.createElement("iframe");
      setVolumeSpy = jasmine.createSpy("setVolume-handler");
      const spies = {
        setVolume: setVolumeSpy
      };
      stubYT(iframe, spies);
      video = createVideo();
    });
    it("sets volume", function() {
      //given
      videoPlayer.addPlayer(video);
      const options = player.i.h;
      const onReady = options.events.onReady;
      const evt = {
        target: player
      };
      //when
      onReady(evt);

      //then
      expect(setVolumeSpy).toHaveBeenCalled();
    });
  });
});

About the only thing left to do now is to hide away those spies into the stubYT() function, but the benefit of that can wait until the next refactoring.

Code is refactored :ballot_box_with_check: Fail :ballot_box_with_check: Pass :ballot_box_with_check: Refactor
Refactoring is all done for now. We can cycle back to the start of the process.

A failing test ☒ Fail ☐ Pass ☐ Refactor

Here is the onPlayer code that we are testing:

  function onPlayerReady(event) {
    player = event.target;
    player.setVolume(100);
    shufflePlaylist(player);
    const iframe = player.h;
    iframe.dispatchEvent(events.afterPlayerReady);
  }

We have just checked setVolume and shufflePlaylist is next, but because that goes into a separate area we’ll check it later.

That means our next test is checking that afterPlayerReady gets dispatched, so after the “sets volume” test we can add another test called “dispatches the afterPlayerReady event”

That test needs to expect that a spy has been called.

I have this: https://jsfiddle.net/k76xL0zo/2/

it("dispatches the afterPlayerReady event", function() {
      //given
      player = undefined;

      //when
      videoPlayer.addPlayer(video);

      //then
      const spy = player.i.h;
      expect(spy).toHaveBeenCalled();
    });

Please just start with only the expect part in the test.

Remove the player=undefined, and remove the const spy part.

https://jsfiddle.net/yj7htqxn/2/

   it("dispatches the afterPlayerReady event", function() {
      //given

      //when
      videoPlayer.addPlayer(video);

      //then
      expect(spy).toHaveBeenCalled();
    });

The error message says that spy is not defined, so we now define a spy in the given section.

The spy is going to be used for the afterPlayerReady event, so the spy variable should have a description of “afterPlayerReady-handler”.

Lines 162 and 272 are where spies are created in your existing code. You can use one of those lines as a template for creating a spy in the new test. I recommend copying line 162 because that’s identical to what you need in the new test.

https://jsfiddle.net/s0d17xgv/1/

 it("dispatches the afterPlayerReady event", function() {
      //given
      const spy = jasmine.createSpy("afterPlayerReady-handler");
      
      //when
      videoPlayer.addPlayer(video);

      //then
      expect(spy).toHaveBeenCalled();
    });

Test fails :ballot_box_with_check: Fail ☐ Pass ☐ Refactor
We now have a suitably failing test which says:
Expected spy afterPlayerReady-handler to have been called.

Make test pass :ballot_box_with_check: Fail ☒ Pass ☐ Refactor
We need to call onReady() in the same way that was done with the previous test.

I want to do all sorts of improvements right now, but before that we need to get this test passing. In getting this test passing we’ll gain good clear info about what needs to be improved.

The addPlayer part of the test doesn’t do anything useful for us in this test, so remove that line.

We need to pass that spy into the init code, so immediately after the spy, call initVideoPlayer() with a first argument of spy

Then update the initVideoPlayer() function so that has a spy parameter.

    function initVideoPlayer(spy = {}) {
	  videoPlayer.init({
        afterPlayerReady: spy
      });      
      removeIframeScripts();
    }

Then copy the given and when sections from the “sets volume” test into the test below it that we’re working on. That will get the test working, after which there’s quite a bit of refactoring to do.

1 Like

Test passes: https://jsfiddle.net/weda9q1r/

Test passes :ballot_box_with_check: Fail :ballot_box_with_check: Pass ☐ Refactor
The test passes, and we can now work on some refactoring.

Refactor the code :ballot_box_with_check: Fail :ballot_box_with_check: Pass ☒ Refactor
Scanning over the code for the playerReady tests, several things are seen to be improved.

One of those improvements is to the definition of the iframe variable. It isn’t being referenced by anything outside of the beforeEach code, so we should push that definition down into the beforeEach code.

How we achieve that is by updating the beforeEach iframe to be the definition (by adding const to the front of iframe), and removing the now unused iframe definition (that is the let iframe line).

I did that here: https://jsfiddle.net/n4hoydt8/

It doesn’t look to have been done at all.

I’ll compare your previous code with the recent code to find out the difference.
You made that change to the “addPlayer” section of tests, when you were instead asked to make the change to the “playerReady tests” section.

Fortunately, the iframe in the “addPlayer” section of tests also needed that update, so please also do the same to the iframe in the “playerReady tests” section that you were asked to do.

Also, that “playerReady tests” section can do with being renamed to be just “playerReady” so that its name on the tests result page is consistent with the others.

1 Like

I did that here: https://jsfiddle.net/t56pqk43/

Carrying on with the refactoring in the playerReady tests, that setVolumeSpy variable is going to have several other brothers and sisters called playVideoAtSpy, setShuffleSpy, stopVideoSpy. Or at least, it will if we don’t do something now to improve the situation.

Instead of having several different spies, we can have just the one spies array, and assign it from the function call to stubYT().

Before doing that though we need to remove the existing use of the spies variable in the playReady beforeEach function, by inlining the spies object into the stubYT() function call.

I don’t understand how to do that.


describe("playerReady", function() {
    let setVolumeSpy;
    let video;
    
    function initVideoPlayer(spy = {}) {
      videoPlayer.init({
        afterPlayerReady: spy
      });
      removeIframeScripts();
    }

    beforeEach(function() {
      initVideoPlayer();
      const iframe = document.createElement("iframe");
      setVolumeSpy = jasmine.createSpy("setVolume-handler");
      const spies = {
        setVolume: setVolumeSpy
      };
      stubYT(iframe, spies);
      video = createVideo();
    });

The process of inlining a variable is the opposite of extracting a variable.

What follows is a summary of how we:

  • Extract a variable
  • Inline a variable

Extract a variable

In this example we start with the following code:

      stubYT(iframe, {setVolume: setVolumeSpy});

When we have some code may be difficult to understand, we can extract a variable from it.

Because we have plans to add more spies, we want to extract {setVolume: setVolumeSpy} to a variable called spies.

How we extract it out to a separate variable is a three-step process:

  1. Declare a new variable
  2. Assign part of the statement to that variable
  3. Replace that part of the statement with the variable

1. Declare a new variable

Here we add a new variable above the statement from which we are going to extract.

      const spies;
      stubYT(iframe, {setVolume: setVolumeSpy});

2. Assign part of the statement to that variable

We then copy some of the statement as an assignment to the new variable.

      // const spies;
      const spies = {setVolume: setVolumeSpy};
      stubYT(iframe, {setVolume: setVolumeSpy});

3. Replace that part of the statement with the variable

Then we complete the extraction process by replacing some of the statement with the variable.

      const spies = {setVolume: setVolumeSpy};
      // stubYT(iframe, {setVolume: setVolumeSpy});
      stubYT(iframe, spies);

Aside: We could then update the spies object so that multiple items can be added to it, such as with the following:

      const spies = {
        playVideoAt: playVideoAtSpy,
        setShuffle: setShuffleSpy,
        setVolume: setVolumeSpy,
        stopVideo: stopVideoSpy
      };

However, we won’t be doing that with the code just yet. Not until the spies are neatly contained inside of the stubYT() function.

Inlining a variable

The process of inlining a variable is the opposite of extracting a variable.

Here is the sample code that we are starting with, where we want to inline the spies variable.

      const spies = {setVolume: setVolumeSpy};
      stubYT(iframe, spies);

Inlining a variable is done when we no longer have need for that variable.

How we inline a variable is a two-step process:

  1. Replace the variable with the variable assignment
  2. Remove the variable

1. Replace the variable with the variable assignment

Everywhere that the variable is used, we replace that variable with what is assigned to it.

      const spies = {setVolume: setVolumeSpy};
      // stubYT(iframe, spies);
      stubYT(iframe, {setVolume: setVolumeSpy});

2. Remove the variable

Now that the variable is not being used, we can remove it.

      // const spies = {setVolume: setVolumeSpy};
      stubYT(iframe, {setVolume: setVolumeSpy});

That is how to extract a variable, and how to inline a variable.