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
Related
I've got a functional script on my site that allows me to open a link in a new window when a specific class is added to the link. I need a lot of those on my site though so I figured I'd make the script a bit easier to edit by working with variables.
In the process of changing out hardcoded strings for variables my script stopped working though. The only one that works is the var where I set the url.
I'm learning that ${} doesn't work everywhere. Hope that someone can point out where my thinking is wrong. Also hope that I got the terminology right, trying to learn though! :-)
var function1Name = "test_function";
var function1Url = "https://www.google.com";
var function1Class = ".test_function_class";
function ${function1Name}() {
window.open(function1Url, "_blank", "height=200");
}
jQuery("${function1Class}").click(function(){
${function1Name}()
});
None of your uses of ${} are valid JavaScript syntax.
Your function declaration van be replaced with:
window[function1Name] = function () {
window.open(function1Url, "_blank", "height=200");
}
Please note that the function will no longer be hoisted when declared this way, so order of operation matters.
The click handler can be written as follows:
jQuery(function1Class).click(function() { // Note that I just used the variable there.
window[function1Name]();
});
There is a ${} concept in JavaScript, but that is only in template literals:
const myVariable = "Foo";
const message = `myVariable contains: "${myVariable}"!`;
console.log(message);
There's several syntax issues here.
Firstly, function ${function1Name}() is not valid syntax. Function names must be defined before runtime. If you want to dynamically access a function, place it in an object and set the key with the variable reference.
Secondly, ${function1Name}() is again not valid syntax. You cannot invoke a function like that dynamically. Referring to the suggestion above, you can access an object dynamically so the first point fixes this problem.
Thirdly, string interpolation only works within template literals, so you need to delimit the string with backticks: ``. However it's completely redundant here as you can just use $(function1Class)
With those issues in mind, here's an updated example:
var function1Name = "test_function";
var function1Url = "https://www.google.com";
var function1Class = ".test_function_class";
var funcObj = {
[function1Name]: function() {
console.log(`function called, window would open here containing ${function1Url}...`);
// window.open(function1Url, "_blank", "height=200");
}
}
$(function1Class).click(function() {
funcObj[function1Name]()
});
/*
alternative using a template literal, although note that it's redundant here
$(`${function1Class}`).click(function() {
funcObj[function1Name]()
});
*/
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Click me
One last thing to note is that no version of IE supports template literals, so be sure of your browser support requirements before using them.
So cool, I got it to work!
var function1Name = "test_function_1";
var function1Url = "https://www.google.com";
var function1Class = ".test_function_class1";
var function2Name = "test_function_2";
var function2Url = "https://www.cnn.com";
var function2Class = ".test_function_class2";
// Function 1
window[function1Name] = function () {
window.open(function1Url, "_blank", "toolbar=no,status=no,scrollbars=yes,resizable=yes,top=500,left=500,width=600,height=745");
}
jQuery(function1Class).click(function() {
window[function1Name]();
});
// Function 2
window[function2Name] = function () {
window.open(function2Url, "_blank", "toolbar=no,status=no,scrollbars=yes,resizable=yes,top=500,left=500,width=600,height=745");
}
jQuery(function2Class).click(function() {
window[function2Name]();
});
I can now add a bunch of url's and corresponding classes as was my intention. Super happy about that.
A follow up question though, as I'll have to experiment with what the ideal window parameters will be I'm trying to make those arguments variables as well. I've tried the examples of how to insert a variables output from the functional code but those methods don't work there. Here's that code:
var windowWidth = 250
var function1Name = "test_function_1";
var function1Url = "https://www.google.com";
var function1Class = ".test_function_class1";
var function2Name = "test_function_2";
var function2Url = "https://www.cnn.com";
var function2Class = ".test_function_class2";
// Function 1
window[function1Name] = function () {
window.open(function1Url, "_blank", "toolbar=no,status=no,scrollbars=yes,resizable=yes,top=500,left=500,width=[windowWidth],height=745");
}
jQuery(function1Class).click(function() {
window[function1Name]();
});
// Function 2
window[function2Name] = function () {
window.open(function2Url, "_blank", "toolbar=no,status=no,scrollbars=yes,resizable=yes,top=500,left=500,width=600,height=745");
}
jQuery(function2Class).click(function() {
window[function2Name]();
});
How would I insert the variables value (2nd line of Function1) there ?
I'm writing some test code for work and am trying to figure out why it throws an error. I'm using an object oriented approach to this so to keep my main script as clean as possible. I have one script that contains all my element paths called elements.js. I'm using phantom to inject that script into another file that contains click methods called click.js. Example:
function Click() {
phantom.page.includeJs('/my/path/to/elements.js');
var element = new Elements();
this.clickElement = function() {
casper.then( function() {
casper.click(element.nameOfElement);
});
};
}
Running my script this way throws undefined errors, however if I directly declare the element's path in my click.js script, my test runs fine:
function Click() {
var nameOfElement = ('css > path > to > element');
this.clickElement = function() {
casper.then( function() {
casper.click(nameOfElement);
});
};
}
I'm wanting to call all element paths from one source just to keep my scripts clean, as you can imagine they can get long depending on how many elements we're testing. I also have other files that require the use of an element's path for this test, but they follow the same principle as the piece of my click.js script above.
UPDATE
Here is what my element.js script looks like:
function Elements() {
var nameOfElement = ("css path");
var anotherElement = ("css path");
}
PhantomJS (and CasperJS) has two contexts. page.includeJs() and page.injectJs() are two PhantomJS functions that include a JavaScript file into the DOM which is then evaluated in the page context, but you want to access your elements paths outside of the page context.
I assume your elements.js looks like this:
function Elements(){
this.someElement = "css path";
...
}
then you need to read the file and evaluate it:
var fs = require("fs");
var el = fs.read("/path/tp/elements.js");
eval(el);
var elements = new Elements();
console.log(elements.someElement);
You probably should define your elements.js differently to leverage PhantomJS's require capability:
As module
elements.js
module.exports = function(){
this.someElement = "css path";
...
}
script.js
var elements = new require("/path/to/elements.js");
As object
elements.js
exports.someElement = "css path";
exports.someOtherElement = "css path";
script.js
var elements = require("/path/to/elements.js");
You probably don't want to read the file on every click, so you can
var elements = require("/path/to/elements.js");
casper.clickElement = function(nameOfElement) {
this.thenClick(elements[nameOfElement]);
};
and use it later like this
casper.clickElement("someOtherElement");
I solved this by getting through a rookie brain fart I was in. In my element.js script, I added a simple get method and it works now.
function Elements() {
var someElement = "css path";
this.getSomeElementPath = function() {
return someElement;
};
}
Called it in my click.js script as:
function Click() {
phantom.page.injectJs('path/to/element.js');
var element = new Elements();
this.clickSomeElement = function() {
casper.then( function() {
casper.click(element.getSomeElementPath());
});
};
}
And, in my main script:
'use strict':
phantom.page.injectJs('/path/to/element.js');
phantom.page.injectJs('/path/to/click.js');
var element = new Element();
var click = new Click();
casper.test.begin("Test", function (test) {
var url = www.url.com;
console.log(url);
casper.start(url);
casper.then(function() {
click.clickSomeElement();
});
....//rest of code
casper.run(function() {
casper.test.done();
});
});
Now, the test runs as it should.
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();
When running my javascript program on IE, it loops forever.
It works on Firefox, Safari and Chrome.
There is a similar question in Stackoverflow, but here the suggestion was to remove the functionname of the embedded function. I don't have a name on it.
Here an excerpt of the function to set up an asynchronous httprequest. 'rq' is the request object, with open() already called, but not send().
I am trying to set a response function. I am using the 'self' var so that the function will have the same this pointer (also member of same object as calling function).
var self = this;
rq.onreadystatechange = function()
{
self._reqStateChange(rq); // *
} // *
this._req[sl]=rq;
rq.send(null);
return true;
IE endless loops the two lines marked with // *, also the first return in the embedded function:
this._reqStateChange = function(rq)
{
if( (!rq) || rq.readyState!=4 )
return; // exits here every time.
IE 9.0, using built-in debugger.
no jQuery or any other libs inplace.
I have no idea. thanks for helping. I would like to understand the logic here...
Note:
I just built a little script to see if the self-construct is wrong. That works. Seems to be related to the onreadystatechange function. Everytime it exits, it is immediately called again.
This code does NOT lock up in IE:
someObject = function()
{
this.callback = function(){}
this.funcCallsCallback = function()
{
// call the callback
this.callback();
};
}
muell = new function()
{
this.value = 42;
this.test = function()
{
var rq = new someObject();
var self = this;
rq.callback = function()
{
self.endFunc();
}
// now:
rq.funcCallsCallback();
};
this.endFunc = function()
{
alert("called! value="+this.value.toString());
};
}
muell.test();
There are many topics related to my question and i have been through most of them, but i haven't got it right. The closest post to my question is the following:
How to call functions that are nested inside a JQuery Plugin?
Below is the jquery plugin i am using. On resize, the element sizes are recalculated. I am now trying to call the function resizeBind() from outside of the jquery plugin and it gives me error
I tried the following combinations to call the function
$.fn.splitter().resizeBind()
$.fn.splitter.resizeBind()
Any ideas, where i am getting wrong?
;(function($){
$.fn.splitter = function(args){
//Other functions ......
$(window).bind("resize", function(){
resizeBind();
});
function resizeBind(){
var top = splitter.offset().top;
var wh = $(window).height();
var ww = $(window).width();
var sh = 0; // scrollbar height
if (ww <0 && !jQuery.browser.msie )
sh = 17;
var footer = parseInt($("#footer").css("height")) || 26;
splitter.css("height", wh-top-footer-sh+"px");
$("#tabsRight").css("height", splitter.height()-30+"px");
$(".contentTabs").css("height", splitter.height()-70+"px");
}
return this.each(function() {
});
};
})(jQuery);
I had the same problem. Those answers on related posts didn't work for my case either. I solved it in a round about way using events.
The example below demonstrates calling a function that multiplies three internal data values by a given multiplier, and returns the result. To call the function, you trigger an event. The handler in turn triggers another event that contains the result. You need to set up a listener for the result event.
Here's the plugin - mostly standard jQuery plugin architecture created by an online wizard:
(function($){
$.foo = function(el, options){
// To avoid scope issues, use 'base' instead of 'this'
var base = this;
// Access to jQuery and DOM versions of element
base.$el = $(el);
base.el = el;
// Add a reverse reference to the DOM object
base.$el.data("foo", base);
base.init = function(){
base.options = $.extend({},$.foo.defaultOptions, options);
// create private data and copy in the options hash
base.private_obj = {};
base.private_obj.value1 = (base.options.opt1);
base.private_obj.value2 = (base.options.opt2);
base.private_obj.value3 = (base.options.opt3);
// make a little element to dump the results into
var ui_element = $('<p>').attr("id","my_paragraph").html(base.private_obj.value1 +" "+ base.private_obj.value2+" " +base.private_obj.value3);
base.$el.append(ui_element);
// this is the handler for the 'get_multiplied_data_please' event.
base.$el.bind('get_multiplied_data_please', function(e,mult) {
bar = {};
bar.v1 = base.private_obj.value1 *mult;
bar.v2 = base.private_obj.value2 *mult;
bar.v3 = base.private_obj.value3 *mult;
base.$el.trigger("here_is_the_multiplied_data", bar);
});
};
base.init();
}
$.foo.defaultOptions = {
opt1: 150,
opt2: 30,
opt3: 100
};
$.fn.foo = function(options){
return this.each(function(){
(new $.foo(this, options));
});
};
})(jQuery);
So, you can attach the object to an element as usual when the document is ready. And at the same time set up a handler for the result event.
$(document).ready(function(){
$('body').foo();
$('body').live('here_is_the_multiplied_data', function(e, data){
console.log("val1:" +data.v1);
console.log("val2:" +data.v2);
console.log("val3:" +data.v3);
$("#my_paragraph").html(data.v1 +" "+ data.v2+" " +data.v3);
});
})
All that's left is to trigger the event and pass it a multiplier value
You could type this into the console - or trigger it from a button that picks out the multiplier from another UI element
$('body').trigger('get_multiplied_data_please', 7);
Disclaimer ;) - I'm quite new to jQuery - sorry if this is using a hammer to crack a nut.
resizeBind function is defined as private so you cannot access it from outside of it's scope. If you want to use it in other scopes you need to define it like that
$.fn.resizeBind = function() { ... }
Then you would call it like that $(selector').resizeBind()
You have defined the resizeBind function in a scope that is different from the global scope. If you dont'use another javascript framework or anything else that uses the $ function (to prevent conflict) you can delete the
(function($){
...
})(jQuery);
statement and in this way the function will be callable everywhere without errors
I didn't test it:
this.resizeBind = function() { .... }