Setting up single-player tests before adding spinner

🗹 Fail 🗹 Pass ☐ Refactor

Very good. Thanks to our preparation in the previous test, the current test is all passing appropriately.

🗹 Fail 🗹 Pass Refactor

is there any refactoring to be done? Yes there is.

Most of the tests refer to when clicked, so we should use a describe section for when clicked, and separate out the tests that aren’t for when clicked.

We can start at that by adding a describe section called “when clicked” above the first test, and close off the describe section after the last test.

Then we can move out any tests that don’t relate to when clicked up above that description, and we can then remove things relating to when clicked from the test descriptions.

I did that here: https://jsfiddle.net/6rbah0se/1/

  it("defines the afterClickCover event", function() {

    //given
    const callbackSpy = jasmine.createSpy("afterClickCover-callback");

    manageCover.init(callbackSpy);

    //when
    simulateAfterClickCover(cover);

    //then
    expect(callbackSpy).toHaveBeenCalled();

  });

  describe("when clicked", function() {

    it("uninitialized cover doesn’t hide", function() {

      //given
      cover.classList.remove("hide");

      //when
      simulateClick(cover);

      //then
      expect(cover).not.toHaveClass("hide");
    });

    it("hides the cover", function() {

      //given
      manageCover.init();

      cover.classList.remove("hide");

      //when
      simulateClick(cover);

      //then
      expect(cover).toHaveClass("hide");
    });

    it("slides the curtain", function() {

      //given
      manageCover.init();

      curtain.classList.remove("slide");

      //when
      simulateClick(cover);

      // then
      expect(curtain).toHaveClass("slide");
    });

    it("shows the video", function() {

      //given
      manageCover.init();

      wrap.classList.add("hide");

      //when
      simulateClick(cover);

      //then
      expect(wrap).not.toHaveClass("hide");
    });

    it("dispatches the afterClickCover event", function() {

      //given
      const callbackSpy = jasmine.createSpy("afterClickCover-callback");

      manageCover.init(callbackSpy);

      //when
      simulateClick(cover);

      //then
      expect(callbackSpy).toHaveBeenCalled();

    });
  });
});

🗹 Fail 🗹 Pass 🗹 Refactor

The refactoring is complete, and the tests have caught up to where they should be.

From now on changes occur to the manageCover code for one of two reasons - refactor or redesign.

A refactor is where the tests all remain as they are, and remain passing while improvements are made to the manageCover code.

A redesign is where we want the manageCover code to have some kind of different behaviour from what it currently does. That means first write a test to give details about the different behaviour that is expected. When we have a suitably failing test because the new behaviour isn’t yet being done, we can then update the manageCover code to achieve that new behaviour, and make the test pass.

1 Like

What are we calling the new test?

   it("spinner fails", function() {


    });

Is any of this being added to the failing test?

      //given
      const callbackSpy = jasmine.createSpy("afterClickCover-callback");

      manageCover.init(callbackSpy);

      //when
      simulateClick(cover);

      //then
      expect(callbackSpy).toHaveBeenCalled();

Here is the css for the spinner.

I added it to the css. https://jsfiddle.net/d4a7fLsc/

/* Spinner */
.lds-dual-ring:after {
  content: " ";
  display: block;
  width: 64px;
  height: 64px;
  margin: auto;
  border-radius: 50%;
  border: 6px solid #fff;
  border-color: #fff transparent #fff transparent;
  animation: lds-dual-ring 1.2s linear infinite;
  opacity: 0.5;
}

@keyframes lds-dual-ring {
  0% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(360deg);
  }
}

Am I adding this to the manageCover code?

    function toggleSpinner(cover) {
        cover.classList.toggle("lds-dual-ring");
    }

If I am, where am I placing it in here?

const manageCover = (function makeManageCover() {

  const events = {};

  function show(el) {
    el.classList.remove("hide");
  }

  function hide(el) {
    el.classList.add("hide");
  }

  function openCurtain(cover) {
    hide(cover);
    const curtain = document.querySelector(".curtain");
    curtain.classList.add("slide");
    return curtain;
  }

Maybe this would be good for a passing test, but shorter.

As soon as a video has loaded which only takes a second, the spinner goes away and you can click on the cover to start playing.

Also, how do we test the code without a video being loaded?

How does it know a video has been loaded without the videoPlayer code added in?

This part: const videoPlayer = (function makeVideoPlayer() {

Maybe that comes later.

Before any test gets written, we need to be able to explain in words the different behaviour that is wanted from the code. That way we can be more sure that we understand what needs to get done.

This would be for a passing test.

video loaded, spinner goes away.

I don’t know what we want a failing test to show.

We want the spinner to start when the player is added, and to stop spinning when the player is ready.

Which was from here: post #102

Something that we learned early on is that each module should only have a small area of interest. The manageCover code should only have aspects of the cover to deal with and worry about. It’s not appropriate for the mangeCover code to be concerned with the workings of the spinner.

Fortunately we have the afterClickCover event, for we can add something to that without needing to change anything about how the manageCover code works.

1 Like

What are the steps I need to do now.

What is the first thing I should do in the code?

Here is the working code: https://jsfiddle.net/jvny8brm/

Here is the test code: https://jsfiddle.net/d4a7fLsc/

Before adding any code, we should manually try and get the spinner working by adding the classname of “lds-dual-spin” to an HTML element.

Adding the classname to the button where you’d expect it to be,

  <!--<button class="play" type="button" aria-label="Open"></button>-->
  <button class="play lds-dual-ring" type="button" aria-label="Open"></button>

results in the play button being pushed left, and the spinner spinning to the right of it.

Screenshot from 2022-01-19 16-28-33

We need to figure out how to solve that presentation problem first, before even thinking about doing any tests or scripting code work.

1 Like

I have an idea.

First: Hide the play button.
<button class="play hide" type="button" aria-label="Open"></button>

As soon as a video has loaded, the spinner goes away and the play button becomes visible.

The spinner should be a div, not button. https://jsfiddle.net/kd4jco8t/

  <div class="spinner"></div>

  <button class="play hide" type="button" aria-label="Open"></button>

How would I do that in the code?

Having the spinner go away after a video has loaded, then for the play button to appear, how would I do that?

Which is how I think it should work in the code.

Why can’t we just have the spinner class on the curtain element (not the player)? Isn’t that what the curtain is there for? To be a placeholder until there is something to show? That seems like the most appropriate solution, combined with what seems like a minor CSS update to center the spinner.

Is there a way to determine, or find out how many milliseconds it takes until youtube is ready?

How many milliseconds until the play button is able to be clicked?

I was able to do this:

It tells you the time where YouTube is not ready.

The highest number I have received so far: 617ms

Is there a way to set that up where a mouse click is not needed, where it would give me the time on the screen that YouTube is ready?

https://jsfiddle.net/mdxbq3nL/

// Interval
var interval;

// Counter
var enterDate = new Date();
function secondsSinceEnter()
{
  return (new Date() - enterDate) / 1000;
}

// Event function
function evtFct()
{
  var sec = secondsSinceEnter().toFixed(3);
  if (sec < 10)
    document.querySelector('button').innerText = sec + " seconds";
  else
    document.querySelector('button').innerText = 'You are here like for eternity';
}

// Add interval to keep the current time uptodate
/*interval = setInterval(evtFct, 0);*/ // Call evtFct every tick

// Usage example
document.querySelector('button').onclick = function()
{
  evtFct();
  clearInterval(interval); // Disable interval
}

Here I was able to have the timer appear on the screen. https://jsfiddle.net/jksoe8up/

How would I be able to have the timer stop on its own when youtube is ready?

Is this something that is able to be done?

// Interval
var interval;

// Counter
var enterDate = new Date();
function secondsSinceEnter()
{
  return (new Date() - enterDate) / 1000;
}

// Event function
function evtFct()
{
  var sec = secondsSinceEnter().toFixed(3);
  if (sec < 10)
    document.querySelector('button').innerText = sec + " seconds";
  else
    document.querySelector('button').innerText = 'You are here like for eternity';
}

// Add interval to keep the current time uptodate
interval = setInterval(evtFct, 0); // Call evtFct every tick

// Usage example
document.querySelector('button').onclick = function()
{
  evtFct();
  clearInterval(interval); // Disable interval
}

No, that varies wildly depending on many different factors, ranging from a microsecond and up to infinity.

Are you giving up on the spinner already?

It doesn’t even make sense to use a timer. When things are going well it won’t even last for a second or or so.

We’ll take a pause with the javascript for the spinner because I think I may have an idea that will require only css to do this.

I just did it.
Seen Here: https://jsfiddle.net/2j6kraft/

There was something else I was trying to figure out how to do.

I was trying to add the array to the bottom of the code.

This part:

videoPlayer.init([
  "0dgNc5S8cLI",
  "mnfmQe8Mv1g",
  "CHahce95B1g",
  "2VwsvrPFr9w"
]);

Working code: https://jsfiddle.net/5uh980dk/

function onYouTubeIframeAPIReady() {
    const cover = document.querySelector(".play");
    const wrapper = cover.parentElement;
    const frameContainer = wrapper.querySelector(".video");
    videoPlayer.addPlayer(frameContainer, config.playlist);
  }

  function shufflePlaylist(player) {
    player.setShuffle(true);
    player.playVideoAt(0);
    player.stopVideo();
  }

  function onPlayerReady(event) {
    player = event.target;
    player.setVolume(100); // percent
    shufflePlaylist(player);
  }

  function addPlayer(video, playlist) {

    const config = {
      height: 360,
      host: "https://www.youtube-nocookie.com",
      width: 640
    };
    config.playerVars = {
      autoplay: 0,
      cc_load_policy: 0,
      controls: 1,
      disablekb: 1,
      fs: 0,
      iv_load_policy: 3,
      loop: 1,
      playlist,
      rel: 0
    };
    config.events = {
      "onReady": onPlayerReady
    };
    player = new YT.Player(video, config);

  }

  function play() {
    player.playVideo();
  }

  function init(videos) {
    config.playlist = videos.join();
    loadIframeScript();
    window.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady;
  }

  return {
    addPlayer,
    init,
    play
  };
}());

(function initCover() {
  function coverClickHandler() {
    videoPlayer.play();
  }

  const cover = document.querySelector(".play");
  cover.addEventListener("click", coverClickHandler);
}());

videoPlayer.init([
  "0dgNc5S8cLI",
  "mnfmQe8Mv1g",
  "CHahce95B1g",
  "2VwsvrPFr9w"
]);

Following how it was done in the above code.

I got up to here, where I got stuck: https://jsfiddle.net/x4qs50wz/

Having trouble figuring out how to add:

videoPlayer.init([
  "0dgNc5S8cLI",
  "mnfmQe8Mv1g",
  "CHahce95B1g",
  "2VwsvrPFr9w"
]);

To the bottom.

const videoPlayer = (function makeVideoPlayer() {
  const config = {};
  const events = {};
  const eventHandlers = {};
  let player = null;

  function loadIframeScript() {
    const tag = document.createElement("script");
    tag.src = "https://www.youtube.com/iframe_api";
    const firstScriptTag = document.getElementsByTagName("script")[0];
    firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
  }

  function onYouTubeIframeAPIReady() {
    const cover = document.querySelector(".play");
    const wrapper = cover.parentElement;
    const frameContainer = wrapper.querySelector(".video");
    videoPlayer.addPlayer(frameContainer, config.playlist);
  }

  function shufflePlaylist(player) {
    player.setShuffle(true);
    player.playVideoAt(0);
    player.stopVideo();
  }

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

  function addPlayer(video, playlist) {

    const config = {
      height: 360,
      host: "https://www.youtube-nocookie.com",
      width: 640
    };
    config.playerVars = {
      autoplay: 0,
      cc_load_policy: 0,
      controls: 1,
      disablekb: 1,
      fs: 0,
      iv_load_policy: 3,
      loop: 1,
      playlist,
      rel: 0
    };
    config.events = {
      "onReady": onPlayerReady
    };

    player = new YT.Player(video, config);

    const iframe = player.h;
    const eventHandler = eventHandlers.afterPlayerReady;
    iframe.addEventListener("afterPlayerReady", eventHandler);
  }

  function play() {
    player.playVideo();
  }

  function addEvents(handlers) {
    eventHandlers.afterPlayerReady = handlers.afterPlayerReady;
    events.afterPlayerReady = new Event("afterPlayerReady");
  }

  function init(initEventHandlers, videos) {
    addEvents(initEventHandlers);
    config.playlist = videos.join();
    loadIframeScript();
    window.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady;
  }

  return {
    addPlayer,
    init,
    play
  };
}());

videoPlayer.init({
  afterPlayerReady: function initCover() {
    manageCover.init(function playVideo() {
      videoPlayer.play();
    });
  }
});

The first code uses this:

(function initCover() {
  function coverClickHandler() {
    videoPlayer.play();
  }

  const cover = document.querySelector(".play");
  cover.addEventListener("click", coverClickHandler);
}());

videoPlayer.init([
  "0dgNc5S8cLI",
  "mnfmQe8Mv1g",
  "CHahce95B1g",
  "2VwsvrPFr9w"
]);

And the other code uses this:

This is the piece I’m having trouble figuring out how to add the array to.

I’m confused about how the array would get added to this.

videoPlayer.init({
  afterPlayerReady: function initCover() {
    manageCover.init(function playVideo() {
      videoPlayer.play();
    });
  }
});

This does not work like this:

videoPlayer.init({
  "0dgNc5S8cLI",
  "mnfmQe8Mv1g",
  "CHahce95B1g",
  "2VwsvrPFr9w"

  afterPlayerReady: function initCover() {
    manageCover.init(function playVideo() {
      videoPlayer.play();
    });
  }
});

There is this error:

Uncaught TypeError: Cannot read properties of undefined (reading ‘join’)"

But that is because the code is missing:

  const cover = document.querySelector(".play");
  cover.addEventListener("click", coverClickHandler);
}());
videoPlayer.init([
  "0dgNc5S8cLI",
  "mnfmQe8Mv1g",
  "CHahce95B1g",
  "2VwsvrPFr9w"
]);

Because I don’t know how to add them to here:

videoPlayer.init({
  afterPlayerReady: function initCover() {
    manageCover.init(function playVideo() {
      videoPlayer.play();
    });
  }
});

I don’t know how to combine: this

(function initCover() {
  function coverClickHandler() {
    videoPlayer.play();
  }

  const cover = document.querySelector(".play");
  cover.addEventListener("click", coverClickHandler);
}());

videoPlayer.init([
  "0dgNc5S8cLI",
  "mnfmQe8Mv1g",
  "CHahce95B1g",
  "2VwsvrPFr9w"
]);

With this:

videoPlayer.init({
  afterPlayerReady: function initCover() {
    manageCover.init(function playVideo() {
      videoPlayer.play();
    });
  }
});

I’m confused.

This is where we go back to tests. The videoPlayer code is missing its tests. Those need to be added.

When there are tests in place, those help to keep us on track while adding more features. You haven’t experience the benefit of that yet.

1 Like

Here is the working Spinner Code: https://jsfiddle.net/89Leo0dq/

Here is the broken spinner code: https://jsfiddle.net/89Leo0dq/1/

Missing array at the bottom because I don’t know how to add it in.

Here is the working code with the array at the bottom: https://jsfiddle.net/5uh980dk/

What I seem to be having trouble with figuring out is.

Combining this part:

(function initCover() {
  function coverClickHandler() {
    videoPlayer.play();
  }

  const cover = document.querySelector(".play");
  cover.addEventListener("click", coverClickHandler);
}());

videoPlayer.init([
  "0dgNc5S8cLI",
  "mnfmQe8Mv1g",
  "CHahce95B1g",
  "2VwsvrPFr9w"
]);

With the afterPlayerReady / spinner code:

videoPlayer.init({
  afterPlayerReady: function initCover() {
    manageCover.init(function playVideo() {
      videoPlayer.play();
    });
  }
});

Here are the tests were done to the manageCover code: https://jsfiddle.net/m8xL9e1o/

To add tests to the videoPlayer code.

How do I set that up?

What are the first few things I should do?

Normally there would be just the one file for the videoPlayer code, which is used both by the test and also by the project. That’s not an option for us here so we’ll have a separate jsfiddle page for the videoPlayer code tests instead.

Start with the manageCover test page, rename the page description so that it’s more appropriate for the videoPlayer test page, and remove all of the JavaScript code. Put the videoPlayer code in that JavaScript section, and after it place a describe section with a description of “videoPlayer tests”. The videoPlayer code has several methods, those being init, addPlayer, and play. It is inside of that describe section that we will build tests for each of those methods. The purpose of the tests is twofold. One, it acts as documentation about the proper way to use those methods, and two, they ensure that the code keeps on behaving exactly as we expect that it should when we refactor the code, and use other tests to redesign the code.

1 Like

rename the page description

I do not know what you mean by that, where that is in the code.

I will continue and come back to that.

I still do not know what you mean by page description.

I don’t believe the phrase “page description” was ever used in developing this test code. I do not know where that is.

Wasn’t I supposed to remove manageCover?

You said remove all of the javascript.

“Script error.”

Uncaught ReferenceError: manageCover is not defined"

I have this: https://jsfiddle.net/jso1a65d/

const videoPlayer = (function makeVideoPlayer() {
  const events = {};
  const eventHandlers = {};
  let player = null;

  function loadIframeScript() {
    const tag = document.createElement("script");
    tag.src = "https://www.youtube.com/iframe_api";
    const firstScriptTag = document.getElementsByTagName("script")[0];
    firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
  }

  function onYouTubeIframeAPIReady() {
    const cover = document.querySelector(".play");
    const wrapper = cover.parentElement;
    const frameContainer = wrapper.querySelector(".video");
    videoPlayer.addPlayer(frameContainer);
  }

  function shufflePlaylist(player) {
    player.setShuffle(true);
    player.playVideoAt(0);
    player.stopVideo();
  }

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

  function addPlayer(video) {

    const playlist = "0dgNc5S8cLI,mnfmQe8Mv1g,-Xgi_way56U,CHahce95B1g";

    const config = {
      height: 360,
      host: "https://www.youtube-nocookie.com",
      width: 640
    };
    config.playerVars = {
      autoplay: 0,
      cc_load_policy: 0,
      controls: 1,
      disablekb: 1,
      fs: 0,
      iv_load_policy: 3,
      loop: 1,
      playlist,
      rel: 0
    };
    config.events = {
      "onReady": onPlayerReady
    };

    player = new YT.Player(video, config);

    const iframe = player.h;
    const eventHandler = eventHandlers.afterPlayerReady;
    iframe.addEventListener("afterPlayerReady", eventHandler);
  }

  function play() {
    player.playVideo();
  }

  function addEvents(handlers) {
    eventHandlers.afterPlayerReady = handlers.afterPlayerReady;
    events.afterPlayerReady = new Event("afterPlayerReady");
  }

  function init(initEventHandlers) {
    addEvents(initEventHandlers);
    loadIframeScript();
    window.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady;
  }

  return {
    addPlayer,
    init,
    play
  };
}());

videoPlayer.init({
  afterPlayerReady: function initCover() {
    manageCover.init(function playVideo() {
      videoPlayer.play();
    });
  }
});
describe("videoPlayer tests", function() {

});