I have some unit tests for a function that makes use of the window.location.href -- not ideal I would far rather have passed this in but its not possible in the implementation. I'm just wondering if its possible to mock this value without actually causing my test runner page to actually go to the URL.
window.location.href = "http://www.website.com?varName=foo";
expect(actions.paramToVar(test_Data)).toEqual("bar");
I'm using jasmine for my unit testing framework.
The best way to do this is to create a helper function somewhere and then mock that:
var mynamespace = mynamespace || {};
mynamespace.util = (function() {
function getWindowLocationHRef() {
return window.location.href;
}
return {
getWindowLocationHRef: getWindowLocationHRef
}
})();
Now instead of using window.location.href directly in your code simply use this instead. Then you can replace this method whenever you need to return a mocked value:
mynamespace.util.getWindowLocationHRef = function() {
return "http://mockhost/mockingpath"
};
If you want a specific part of the window location such as a query string parameter then create helper methods for that too and keep the parsing out of your main code. Some frameworks such as jasmine have test spies that can not only mock the function to return desired values, but can also verified it was called:
spyOn(mynamespace.util, 'getQueryStringParameterByName').andReturn("desc");
//...
expect(mynamespace.util.getQueryStringParameterByName).toHaveBeenCalledWith("sort");
I would propose two solutions which have already been hinted at in previous posts here:
Create a function around the access, use that in your production code, and stub this with Jasmine in your tests:
var actions = {
getCurrentURL: function () {
return window.location.href;
},
paramToVar: function (testData) {
...
var url = getCurrentURL();
...
}
};
// Test
var urlSpy = spyOn(actions, "getCurrentURL").andReturn("http://my/fake?param");
expect(actions.paramToVar(test_Data)).toEqual("bar");
Use a dependency injection and inject a fake in your test:
var _actions = function (window) {
return {
paramToVar: function (testData) {
...
var url = window.location.href;
...
}
};
};
var actions = _actions(window);
// Test
var fakeWindow = {
location: { href: "http://my/fake?param" }
};
var fakeActions = _actions(fakeWindow);
expect(fakeActions.paramToVar(test_Data)).toEqual("bar");
You need to simulate local context and create your own version of window and window.location objects
var localContext = {
"window":{
location:{
href: "http://www.website.com?varName=foo"
}
}
}
// simulated context
with(localContext){
console.log(window.location.href);
// http://www.website.com?varName=foo
}
//actual context
console.log(window.location.href);
// http://www.actual.page.url/...
If you use with then all variables (including window!) will firstly be looked from the context object and if not present then from the actual context.
Sometimes you may have a library that modifies window.location and you want to allow for it to function normally but also be tested. If this is the case, you can use a closure to pass your desired reference to your library such as this.
/* in mylib.js */
(function(view){
view.location.href = "foo";
}(self || window));
Then in your test, before including your library, you can redefine self globally, and the library will use the mock self as the view.
var self = {
location: { href: location.href }
};
In your library, you can also do something like the following, so you may redefine self at any point in the test:
/* in mylib.js */
var mylib = (function(href) {
function go ( href ) {
var view = self || window;
view.location.href = href;
}
return {go: go}
}());
In most if not all modern browsers, self is already a reference to window by default. In platforms that implement the Worker API, within a Worker self is a reference to the global scope. In node.js both self and window are not defined, so if you want you can also do this:
self || window || global
This may change if node.js really does implement the Worker API.
Below is the approach I have take to mock window.location.href and/or anything else which maybe on a global object.
First, rather than accessing it directly, encapsulate it in a module where the object is kept with a getter and setter. Below is my example. I am using require, but that is not necessary here.
define(["exports"], function(exports){
var win = window;
exports.getWindow = function(){
return win;
};
exports.setWindow = function(x){
win = x;
}
});
Now, where you have normally done in your code something like window.location.href, now you would do something like:
var window = global_window.getWindow();
var hrefString = window.location.href;
Finally the setup is complete and you can test your code by replacing the window object with a fake object you want to be in its place instead.
fakeWindow = {
location: {
href: "http://google.com?x=y"
}
}
w = require("helpers/global_window");
w.setWindow(fakeWindow);
This would change the win variable in the window module. It was originally set to the global window object, but it is not set to the fake window object you put in. So now after you replaced it, the code will get your fake window object and its fake href you had put it.
This works for me:
delete window.location;
window.location = Object.create(window);
window.location.href = 'my-url';
This is similar to cpimhoff's suggestion, but it uses dependency injection in Angular instead. I figured I would add this in case someone else comes here looking for an Angular solution.
In the module, probably the app.module add a window provider like this:
#NgModule({
...
providers: [
{
provide: Window,
useValue: window,
},
],
...
})
Then in your component that makes use of window, inject window in the constructor.
constructor(private window: Window)
Now instead of using window directly, use the component property when making use of window.
this.window.location.href = url
With that in place you can set the provider in Jasmine tests using TestBed.
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [
{
provide: Window,
useValue: {location: {href: ''}},
},
],
}).compileComponents();
});
IMO, this solution is a small improvement of cburgmer's in that it allows you to replace window.location.href with $window.location.href in the source. Granted I'm using Karma and not Jasmine, but I believe this approach would work with either. And I've added a dependency on sinon.
First a service / singleton:
function setHref(theHref) {
window.location.href = theHref;
}
function getHref(theHref) {
return window.location.href;
}
var $$window = {
location: {
setHref: setHref,
getHref: getHref,
get href() {
return this.getHref();
},
set href(v) {
this.setHref(v);
}
}
};
function windowInjectable() { return $$window; }
Now I can set location.href in code by injecting windowInjectable() as $window like this:
function($window) {
$window.location.href = "http://www.website.com?varName=foo";
}
and mocking it out in a unit test it looks like:
sinon.stub($window.location, 'setHref'); // this prevents the true window.location.href from being hit.
expect($window.location.setHref.args[0][0]).to.contain('varName=foo');
$window.location.setHref.restore();
The getter / setter syntax goes back to IE 9, and is otherwise widely supported according to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set
Here's my generic solution that requires an extra import in production code, but doesn't require dependency injection or writing individual wrapper functions like getHref().
Basically we toss the window into a separate file and then our prod code imports the window indirectly from that file.
In production, windowProxy === window.
In tests we can mutate the module which exports windowProxy and mock it with a new temporary value.
// windowProxy.js
/*
* This file exists solely as proxied reference to the window object
* so you can mock the window object during unit tests.
*/
export default window;
// prod/someCode.js
import windowProxy from 'path/to/windowProxy.js';
export function changeUrl() {
windowProxy.location.href = 'https://coolsite.com';
}
// tests/someCode.spec.js
import { changeUrl } from '../prod/someCode.js';
import * as windowProxy from '../prod/path/to/windowProxy.js';
describe('changeUrl', () => {
let mockWindow;
beforeEach(() => {
mockWindow = {};
windowProxy.default = myMockWindow;
});
afterEach(() => {
windowProxy.default = window;
});
it('changes the url', () => {
changeUrl();
expect(mockWindow.location.href).toEqual('https://coolsite.com');
});
});
You need to fake window.location.href while being on the same page.
In my case, this snipped worked perfectly:
$window.history.push(null, null, 'http://server/#/YOUR_ROUTE');
$location.$$absUrl = $window.location.href;
$location.replace();
// now, $location.path() will return YOUR_ROUTE even if there's no such route
Related
I've been reading up on karma mainly and jasmine a little and have begun to implement testing on my app.
I have the following function :
$scope.popup1 = function (isinData) {
var popup1 = window.open("views/Box_Ladder.html", "_blank",
"height = 400, width = 700");
shareDataService.setIsinClickValue(isinData);
}
How on earth do I test this using karma? The expected result is a popup window opening and the relevant data being passed to my service. How do I expect this?
You spy on window.open and expect it to be called with the right arguments.
Even if the function doesn't return something, it should at the minimum cause some side-effect. You need to test the side-effects.
To do this, create and inject a mock object + object method. An example would be as follows:
var window = {
open: function(url, target, specs) {
var spec, specKey;
this.href = url;
this.target = target;
// Parse through the spec string to grab the parameters you passed through
var specArray = specs.split(',');
for (specKey in specArray) {
spec = specArray[specKey].split('=');
this[String.trim(spec[0])] = String.trim(spec[1]);
}
}
};
Now you can expect(window.href).toEqual(url), expect(window.target).toEqual(target), expect(window.height).toEqual(400), etc.
Additionally, you need to see if sharedDataService.setIsinClickValue was invoked. If you cannot access this service within your test, you're going to have to create another mock object + method.
I'm writing a test where two browsers need to interact. The problem with simply forking the browser is that my page objects still reference the old browser. I didn't want to rewrite all of my PO's to take the browser as a parameter so I tried the first solution found in the link below where they overwrite the global variables with the new browser's version :
Multiple browsers and the Page Object pattern
However, changing the global variables doesn't seem to work as all the subsequent page object functions that I call are performed against the original browser instance. I have tried logging the window handler before and after the switch and they are indeed different which only baffles me further. Here's some of the code.
spec:
var MultiBrowserFunctions = require('../common/multiBrowserFunctions.js');
var HomePage = require('../home/home.po.js');
describe('blah', function(){
it('blah', function(){
MultiBrowserFunctions.openNewBrowser(true);
HomePage.initializePage();
});
});
MultiBrowserFunctions:
(function() {
var browserRegistry = [];
module.exports = {
openNewBrowser: function(isSameUrl){
if(typeof browserRegistry[0] == 'undefined'){
browserRegistry[0] = {
browser: browser,
element: element,
$: $,
$$: $$,
}
}
var tmp = browser.forkNewDriverInstance(isSameUrl);
var id = browserRegistry.length;
browserRegistry[id] = {
browser: tmp,
element: tmp.element,
$: tmp.$,
$$: tmp.$$,
}
switchToBrowserContext(id);
return id;
},
resetBrowserInstance : function(){
browserRegistry.splice(1,browserRegistry.length);
switchToBrowserContext(0);
}
}
function switchToBrowserContext(id){
console.log('---------------------------switching to browser: ' + id);
browser=browserRegistry[id].browser;
element=browserRegistry[id].element;
$=browserRegistry[id].$;
$$=browserRegistry[id].$$;
}
}());
My questions are:
(1) why doesn't this work?
(2) Is there some other solution that doesn't involve rewriting all of my po's?
What you can do is, save the browsers in different variables and then switch between them by overriding the globals via a utility or something.
describe('Switching browsers back and forth', function () {
var browserA, browserB;
it('Browser Switch', function () {
var browsers = {
a : browser,
b : browser.forkNewDriverInstance(true)
};
browserA = browsers.a;
browserB = browsers.b;
var browserAndElement = switchBrowser(browserB);
browser = browserAndElement.browser;
element = browserAndElement.element;
//do your stuff
var browserAndElement = switchBrowser(browserA);
browser = browserAndElement.browser;
element = browserAndElement.element;
//do your stuff
});
});
The switchBrowser() can look like following:
this.switchBrowser = function (currentBrowser) {
browser = currentBrowser;
element = currentBrowser.element;
return {
browser : browser,
element : element
}
}
In this way you don't have to rewrite your POs to take in the new globals.
Hope it helps!
Cheers
We are using the Page Object pattern to organize our internal AngularJS application tests.
Here is an example page object we have:
var LoginPage = function () {
this.username = element(by.id("username"));
this.password = element(by.id("password"));
this.loginButton = element(by.id("submit"));
}
module.exports = LoginPage;
In a single-browser test, it is quite clear how to use it:
var LoginPage = require("./../po/login.po.js");
describe("Login functionality", function () {
var scope = {};
beforeEach(function () {
browser.get("/#login");
scope.page = new LoginPage();
});
it("should successfully log in a user", function () {
scope.page.username.clear();
scope.page.username.sendKeys(login);
scope.page.password.sendKeys(password);
scope.page.loginButton.click();
// assert we are logged in
});
});
But, when it comes to a test when multiple browsers are instantiated and there is the need to switch between them in a single test, it is becoming unclear how to use the same page object with multiple browsers:
describe("Login functionality", function () {
var scope = {};
beforeEach(function () {
browser.get("/#login");
scope.page = new LoginPage();
});
it("should warn there is an opened session", function () {
scope.page.username.clear();
scope.page.username.sendKeys(login);
scope.page.password.sendKeys(password);
scope.page.loginButton.click();
// assert we are logged in
// fire up a different browser and log in
var browser2 = browser.forkNewDriverInstance();
// the problem is here - scope.page.username.clear() would be applied to the main "browser"
});
});
Problem:
After we forked a new browser, how can we use the same Page Object fields and functions, but applied to a newly instantiated browser (browser2 in this case)?
In other words, all element() calls here would be applied to browser, but needed to be applied to browser2. How can we switch the context?
Thoughts:
one possible approach here would be to redefine the global element = browser2.element temporarily while being in the context of browser2. The problem with this approach is that we also have browser.wait() calls inside the page object functions. This means that browser = browser2 should be also set. In this case, we would need to remember the browser global object in a temp variable and restore it once we switch back to the main browser context..
another possible approach would be to pass the browser instance into the page object, something like:
var LoginPage = function (browserInstance) {
browser = browserInstance ? browserInstance : browser;
var element = browser.element;
// ...
}
but this would probably require to change every page object we have..
Hope the question is clear - let me know if it needs clarification.
Maybe you could write few functions to make the the browser registration/start/switch smoother. (Basically it is your first option with some support.)
For example:
var browserRegistry = [];
function openNewBrowser(){
if(typeof browserRegistry[0] == 'undefined'){
browseRegistry[0] = {
browser: browser,
element: element,
$: $,
$$: $$,
... whatever else you need.
}
}
var tmp = browser.forkNewDriverInstance();
var id = browserRegistry.length;
browseRegistry[id] = {
browser: tmp,
element: tmp.element,
$: tmp.$,
$$: tmp.$$,
... whatever else you need.
}
switchToBrowserContext(id);
return id;
}
function switchToBrowserContext(id){
browser=browseRegistry[id].browser;
element=browseRegistry[id].element;
$=browseRegistry[id].$;
$$=browseRegistry[id].$$;
}
And you use it this way in your example:
describe("Login functionality", function () {
var scope = {};
beforeEach(function () {
browser.get("/#login");
scope.page1 = new LoginPage();
openNewBrowser();
browser.get("/#login");
scope.page2 = new LoginPage();
});
it("should warn there is an opened session", function () {
scope.page1.username.clear();
scope.page1.username.sendKeys(login);
scope.page1.password.sendKeys(password);
scope.page1.loginButton.click();
scope.page2.username.clear();
scope.page2.username.sendKeys(login);
scope.page2.password.sendKeys(password);
scope.page2.loginButton.click();
});
});
So you can leave your page objects as they are.
To be honest I think your second approach is cleaner...
Using global variables can bite back later.
But if you don't want to change your POs, this can also work.
(I did not test it... sorry for the likely typos/errors.)
(You can place the support functions to your protractor conf's onprepare section for example.)
Look at my solution. I simplified example, but we are using this approach in current project. My app has pages for both user permissions types, and i need to do some complex actions same time in both browsers. I hope this might show you some new, better way!
"use strict";
//In config, you should declare global browser roles. I only have 2 roles - so i make 2 global instances
//Somewhere in onPrepare() function
global.admin = browser;
admin.admin = true;
global.guest = browser.forkNewDriverInstance();
guest.guest = true;
//Notice that default browser will be 'admin' example:
// let someElement = $('someElement'); // this will be tried to be found in admin browser.
class BasePage {
//Other shared logic also can be added here.
constructor (browser = admin) {
//Simplified example
this._browser = browser
}
}
class HomePage extends BasePage {
//You will not directly create this object. Instead you should use .getPageFor(browser)
constructor(browser) {
super(browser);
this.rightToolbar = ToolbarFragment.getFragmentFor(this._browser);
this.chat = ChatFragment.getFragmentFor(this._browser);
this.someOtherNiceButton = this._browser.$('button.menu');
}
//This function relies on params that we have patched for browser instances in onPrepare();
static getPageFor(browser) {
if (browser.guest) return new GuestHomePage(browser);
else if (browser.admin) return new AdminHomePage(browser);
}
openProfileMenu() {
let menu = ProfileMenuFragment.getFragmentFor(this._browser);
this.someOtherNiceButton.click();
return menu;
}
}
class GuestHomePage extends RoomPage {
constructor(browser) {
super(browser);
}
//Some feature that is only available for guest
login() {
// will be 'guest' browser in this case.
this._browser.$('input.login').sendKeys('sdkfj'); //blabla
this._browser.$('input.pass').sendKeys('2345'); //blabla
this._browser.$('button.login').click();
}
}
class AdminHomePage extends RoomPage {
constructor(browser) {
super(browser);
}
acceptGuest() {
let acceptGuestButton = this._browser.$('.request-admission .control-btn.admit-user');
this._browser.wait(EC.elementToBeClickable(acceptGuestButton), 10000,
'Admin should be able to see and click accept guest button. ' +
'Make sure that guest is currently trying to connect to the page');
acceptGuestButton.click();
//Calling browser directly since we need to do complex action. Just example.
guest.wait(EC.visibilityOf(guest.$('.central-content')), 10000, 'Guest should be dropped to the page');
}
}
//Then in your tests
let guestHomePage = HomePage.getPageFor(guest);
guestHomePage.login();
let adminHomePage = HomePage.getPageFor(admin);
adminHomePage.acceptGuest();
adminHomePage.openProfileMenu();
guestHomePage.openProfileMenu();
I am trying to test angular service which does some manipulations to DOM via $document service with jasmine.
Let's say it simply appends some directive to the <body> element.
Such service could look like
(function(module) {
module.service('myService', [
'$document',
function($document) {
this.doTheJob = function() {
$document.find('body').append('<my-directive></my directive>');
};
}
]);
})(angular.module('my-app'));
And I want to test it like this
describe('Sample test' function() {
var myService;
var mockDoc;
beforeEach(function() {
module('my-app');
// Initialize mock somehow. Below won't work indeed, it just shows the intent
mockDoc = angular.element('<html><head></head><body></body></html>');
module(function($provide) {
$provide.value('$document', mockDoc);
});
});
beforeEach(inject(function(_myService_) {
myService = _myService_;
}));
it('should append my-directive to body element', function() {
myService.doTheJob();
// Check mock's body to contain target directive
expect(mockDoc.find('body').html()).toContain('<my-directive></my-directive>');
});
});
So the question is what would be the best way to create such mock?
Testing with real document will give us much trouble cleaning up after each test and does not look like a way to go with.
I've also tried to create a new real document instance before each test, yet ended up with different failures.
Creating an object like below and checking whatever variable works but looks very ugly
var whatever = [];
var fakeDoc = {
find: function(tag) {
if (tag == 'body') {
return function() {
var self = this;
this.append = function(content) {
whatever.add(content);
return self;
};
};
}
}
}
I feel that I'm missing something important here and doing something very wrong.
Any help is much appreciated.
You don't need to mock the $document service in such a case. It's easier just to use its actual implementation:
describe('Sample test', function() {
var myService;
var $document;
beforeEach(function() {
module('plunker');
});
beforeEach(inject(function(_myService_, _$document_) {
myService = _myService_;
$document = _$document_;
}));
it('should append my-directive to body element', function() {
myService.doTheJob();
expect($document.find('body').html()).toContain('<my-directive></my-directive>');
});
});
Plunker here.
If you really need to mock it out, then I guess you'll have to do it the way you did:
$documentMock = { ... }
But that can break other things that rely on the $document service itself (such a directive that uses createElement, for instance).
UPDATE
If you need to restore the document back to a consistent state after each test, you can do something along these lines:
afterEach(function() {
$document.find('body').html(''); // or $document.find('body').empty()
// if jQuery is available
});
Plunker here (I had to use another container otherwise Jasmine results wouldn't be rendered).
As #AlexanderNyrkov pointed out in the comments, both Jasmine and Karma have their own stuff inside the body tag, and wiping them out by emptying the document body doesn't seem like a good idea.
UPDATE 2
I've managed to partially mock the $document service so you can use the actual page document and restore everything to a valid state:
beforeEach(function() {
module('plunker');
$document = angular.element(document); // This is exactly what Angular does
$document.find('body').append('<content></content>');
var originalFind = $document.find;
$document.find = function(selector) {
if (selector === 'body') {
return originalFind.call($document, 'body').find('content');
} else {
return originalFind.call($document, selector);
}
}
module(function($provide) {
$provide.value('$document', $document);
});
});
afterEach(function() {
$document.find('body').html('');
});
Plunker here.
The idea is to replace the body tag with a new one that your SUT can freely manipulate and your test can safely clear at the end of every spec.
You can create an empty test document using DOMImplementation#createHTMLDocument():
describe('myService', function() {
var $body;
beforeEach(function() {
var doc;
// Create an empty test document based on the current document.
doc = document.implementation.createHTMLDocument();
// Save a reference to the test document's body, for asserting
// changes to it in our tests.
$body = $(doc.body);
// Load our app module and a custom, anonymous module.
module('myApp', function($provide) {
// Declare that this anonymous module provides a service
// called $document that will supersede the built-in $document
// service, injecting our empty test document instead.
$provide.value('$document', $(doc));
});
// ...
});
// ...
});
Because you're creating a new, empty document for each test, you won't interfere with the page running your tests and you won't have to explicitly clean up after your service between tests.
I have a custom matcher in some Jasmine test specs of the form:
this.addMatchers({
checkContains: function(elem){
var found = false;
$.each( this.actual, function( actualItem ){
// Check if these objects contain the same properties.
found = found || actualItem.thing == elem;
});
return found;
}
});
Of course, actualItem.thing == elem doesn't actually compare object contents- I have to use one of the more complex solutions in Object comparison in JavaScript.
I can't help but notice, though, that Jasmine already has a nice object equality checker: expect(x).toEqual(y). Is there any way to use that within a custom matcher? Is there any general way to use matchers within custom matchers?
Yes, it is slightly hacky but entirely possible.
The first thing we need to do is make the Jasmine.Env class available. Personally I have done this in my SpecRunner.html since its already setup there anyway. On the load of my SpecRunner I have the following script that runs:
(function() {
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 1000;
var trivialReporter = new jasmine.TrivialReporter();
jasmineEnv.addReporter(trivialReporter);
jasmineEnv.specFilter = function(spec) {
return trivialReporter.specFilter(spec);
};
var currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
execJasmine();
};
function execJasmine() {
jasmineEnv.execute();
};
})();
So after the execJasmine function declaration I push the jasmineEnv into the global namespace by adding this:
this.jasmineEnv = jasmineEnv;
Now, in any of my spec files I can access the jasmineEnv variable and that is what contains the matchers core code.
Looking at toEqual specifically, toEqual calls the jasmine.Env.prototype.equals_ function. This means that in your customMatcher you can do the following:
beforeEach(function(){
this.addMatchers({
isJasmineAwesome : function(expected){
return jasmineEnv.equals_(this.actual, expected);
}
});
});
Unfortunately, using this method will only give you access to the following methods:
compareObjects_
equals_
contains_
The rest of the matchers reside the jasmine.Matchers class but I have not been able to make that public yet. I hope this helps you out in someway or another