https://jsfiddle.net/q46u1rog/1/
function stubYT(iframe, spies = {}) {
const dummyFunc = () => null;
window.YT = {
Player: function makePlayer(video, options) {
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.
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.
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 Fail
Pass
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 Fail â Pass â Refactor
We now have a suitably failing test which says:
Expected spy afterPlayerReady-handler to have been called.
Make test pass 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.
Test passes: https://jsfiddle.net/weda9q1r/
Test passes Fail
Pass â Refactor
The test passes, and we can now work on some refactoring.
Refactor the code Fail
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.
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:
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:
Here we add a new variable above the statement from which we are going to extract.
const spies;
stubYT(iframe, {setVolume: setVolumeSpy});
We then copy some of the statement as an assignment to the new variable.
// const spies;
const spies = {setVolume: setVolumeSpy};
stubYT(iframe, {setVolume: setVolumeSpy});
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.
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:
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});
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.