Page object pattern with protractor - javascript

I am trying to creat to page object files with protractor.
My app has the following layout.
and page object files..
navbar_po.js
var NavBar = function() {
// define navbar elements and operations
// .
// .
};
module.exports = NavBar;
subNavbar_po.js
var SubNavBar = function() {
// define subnavbar elements and operations
// .
// .
};
module.exports = SubNavBar;
page1_po.js
var Page1 = function() {
this.navbar = function(){
var navbar = require('./navbar_po.js');
return new navbar();
}
this.subnavbar = function(){
var subnavbar = require('./subNavbar_po.js');
return new subnavbar();
}
// define Page1 particular elements and operations
// .
// .
};
module.exports = Page1;
and I access the navbar elements as follows in test script..
var page1 = new require('./page1_po.js');
page1.navbar.something_method();
page1.subnavbar.something_method();
Is this the best way?
I don't want to define the same navbar elements for each page object file.
Is there any other good way?

Ok, great question, super detailed information too!
🛠 Let's get to it. We'll be using ES6 syntax!
Small structural conventions will make the project easier to maintain and won't need defining the same navbar (or any other) elements for each page object file.
Also, the suggested filename kebab-case style will help keep things recognizable as your project grows :)
navbar_po.js // suggest renaming to nav-bar.pageObject.js
export default class NavBar {
constructor() {
this.homePageButton = element(by.id('home-button'))
// more element locators
}
function clickHomePageButton() {
this.homePageButton.click()
}
// more operations
}
subNavbar_po.js // suggest renaming to sub-nav-bar.pageObject.js
export default class SubNavBar {
constructor() {
this.aboutPageButton = element(by.id('about-button'))
// more element locators
}
function clickAboutPageButton() {
this.aboutPageButton.click()
}
// more operations
}
page1_po.js // suggest renaming to page-one.pageObject.js
import SubNavBar from './sub-nav-bar.pageObject' // the .js at the end can be omitted, it will know it's JS!
import navBar from './nav-bar.pageObject'
export default class pageOne {
constructor() {
this.NavBar = NavBar
this.SubNavBar = SubNavBar
// more element locators
}
function visitHomePageThenAboutPage() {
const navBar = new NavBar()
const subNavBar = new SubNavBar
navBar.clickHomePageButton()
subNavBar.clickAboutPageButton()
}
// more operations
}
your test script
pageOne.spec.js
If any of your elements change, you only ever need to change them in the relevant pageObject file. Nothing else needs changing, not even your test script!
import PageOne from './page-one.pageObject');
describe('navigating', () => {
it('should go to home and about', () => {
const pageOne = new PageOne()
pageOne.visitHomePageThenAboutPage()
// expect statement
}
}
💡 pageObject concept
Imagine that your test script mimics human interaction with your site. And pageObjects are how you abstract away and hide exactly what needs to be done for those user behaviours to interact with your page.
📚 Resources
The following is a great styleguide that helps keep things manageable ✨
https://github.com/CarmenPopoviciu/protractor-styleguide
Also, checkout this (disclaimer: my) curated list on all things awesome for Protractor! Make a pull request if you ever want to add anything!
Good luck testing!
https://github.com/chowdhurian/awesome-protractor

Maybe write a utility for that:
// navbarutil.js
var NavbarUtil = {
create: function(page) {
page.navbar = function() {
return new require('./navbar_po.js')();
}
page.subnavbar = function() {
return new require('./subNavbar_po.js')();
}
}
};
module.exports = NavbarUtil;
And in your page use:
var Page1 = function() {
require('./navbarutil.js').create(this);
// Define other things here...
}
module.exports = Page1;

Related

MVC Intersection Observer structure

I wonder if someone can assist me with MVC architecture. I took a course in MVC from Udemy and now I have a pet project I'm working on. In a nutshell. I have three JS files: controller, model and view.
I am watching activeHeading2 element and it a user scrolls past it, manipulates classes on two elements.
Anyways, What's happening now is when a user clicks and displays a new section with new activeHeading2 element, Observer still observes old section activeHeading2 in the view even if I tell it to unobserve or even disconnect. I am stuck on this for like a week now, any information or help would be beneficial.
I am not using any frameworks and this is vanilla JS in action:
// CONTROLLER:
constructor(model, view) {
this.view = view;
this.model = model;
// Init here:
this._cycleHeaderFooter();
this._refreshActiveElements();
this.view.bindToTop(this._handleToTop);
this.view.bindNavClick(this._handleNavClick);
this.view.bindObserver(this._handleObserver);
}
_handleNavClick = clicked => {
//View adjustment here
// Unobserve first before resetting ?
this.view.resetNavigation();
this.view.displaySection(clicked);
this._refreshActiveElements();
this.view.observe();
this.view.displayFooter();
this.view.activateNav(clicked);
}
const app = new Controller(new Model(), new View());
export default class View {
constructor() { }
bindObserver(fn){
// DOM, EVENTS,
fn(this.activeHeading2);
}
observe(activeHeading2){
const toggleObserver= (obs, img) =>{
console.log(obs);
if (obs === 'hide') {
this.main__topEl.classList.add('inactive');
this.headerEl.classList.remove('stickyHeader');
}
if (obs === 'show') {
this.main__topEl.classList.remove('inactive');
this.headerEl.classList.add('stickyHeader');
}
if (obs === 'img') {
// console.log(img.dataset.src);
img.src = img.dataset.src;
// Remove blur filter .lazy-img class
img.classList.remove('lazy-img');
}
}
const callback = function (entries, observer) {
const [entry] = entries;
if (entry.target === activeHeading2) {
entry.isIntersecting ? toggleObserver('hide') : toggleObserver('show');
}
}
const options = {
root: null,
threshold: 0,
}
let heading2Obs = new IntersectionObserver(callback, options);
heading2Obs.unobserve(this.activeHeading2);
heading2Obs.observe(this.activeHeading2);
}
}
Not sure why the view is stuck with old values ?
To run code on instantiation of a class, you need a method called contructor not construction.
Also in your code the view parameter passed into the constructor is not the same as this.view, you need to assign it. See code below as an example of what I mean.
class Controller {
constructor(model, view) {
this.view = view;
this.view.bindObserver(this._handleObserver);
}
_handleObserver = (activeHeading2) => {
this.view.observe(activeHeading2);
}
}
Trick to MVC Architecture is knowing how to import each JavaScript class so that it is accessible to other modules. Example:
class View {
}
export default new Class();
--> Then in controller: import view from './view.js'
this will allow controller to see all elements and methods inside that class.

have separate js objects in a webpack project which interact with same variable

I wanted to know if it was possible by combining webpack and js' oop to arrive at a functional code like the one presented below.
The goal is to be able to isolate each of the elements of my site (sidebar, main,...) in different files while making sure that they can interact between.
Is this possible with webpack and pure js or not?
import ApplePicker from "./my_path/applePicker.js";
import NiceFarmer from "./my_path/niceFarmer.js";
const orchard = function () {
const appleNumber = 10;
const jack = new ApplePicker();
const daniel = new NiceFarmer();
jack.eatAnApple();
daniel.eatAnApple();
// appleNumber have to be now === 2
}
// Example of applePicker.js structure
const ApplePicker = function () {
this.eatAnApple = function () {
// Do something
}
}
export default ApplePicker;
yes you can.
If you simply want to consume a variable from applePicker.js:
// applePicker.js
export const apples = 10
If you want to be able to reassign that variable you might want to add a simple facade layer on top of that:
// applePicker.js
let apple = 5
export function getApple() {
return apple;
}
export function setApple(newValue) {
apple = newValue;
}
Even better, if you want other functions to "fire" when a variable is changed, use the Observer pattern

Objects duplicating every component mount. How can I make it run only once ? In react

My object is a independent js file that I created.
componentDidMount() {
const node = ReactDOM.findDOMNode(this);
const widgetBuild = new window.WidgetFormBuilder({
form: $(node).parents('#dynamic_form_wrapper')
});
widgetBuild.initForm();
}
It is a little hard to workout without knowing more about WidgetFormBuilder.
However as good practice I would suggest...
componentDidMount() {
const node = ReactDOM.findDOMNode(this);
// Assign to the class instance
this.widgetBuild = new window.WidgetFormBuilder({
form: $(node).parents('#dynamic_form_wrapper')
});
this.widgetBuild.initForm();
}
componentWillUnmount() {
// Cleanup
// Check if WidgetFormBuilder has a destroy method or something similar.
// See https://reactjs.org/docs/react-component.html#componentwillunmount
this.widgetBuild = null;
}
shouldComponentUpdate() {
// Stop further re-renders, given you're using the DOM directly this could help prevent a few performance issues
// See https://reactjs.org/docs/react-component.html#shouldcomponentupdate
return false;
}
Finally, take a look at the react docs on third party libs.
I already fixed i just added a .destroy() function in my WidgetFormBuilder. :)
WidgetFormBuilder.prototype.destroyBuilder = function () {
const self = this;
const destroyEvents = function () {
$(self.form).unbind();
};
destroyEvents();
return this;
};

Basic ES6 Javascript Plugin - reuse variable between functions

I'm attempting to build a basic JS plugin that can be called after a click event to disable a button (to prevent users firing multiple API calls) and to give feedback that something is loading/happening. Here is how it looks:
This works great on an individual basis, but I want to re-write it as a plugin so I can reuse it across the site.
Here is a cut down version of the JS from file loader.plugin.js.
let originalBtnText;
export function showBtnLoader(btn, loadingText) {
const clickedBtn = btn;
const spinner = document.createElement('div');
spinner.classList.add('spin-loader');
originalBtnText = clickedBtn.textContent;
clickedBtn.textContent = loadingText;
clickedBtn.appendChild(spinner);
clickedBtn.setAttribute('disabled', true);
clickedBtn.classList.add('loading');
return this;
}
export function hideBtnLoader(btn) {
const clickedBtn = btn.target;
clickedBtn.textContent = originalBtnText;
clickedBtn.removeAttribute('disabled');
clickedBtn.classList.remove('loading');
return this;
}
export function btnLoader() {
showBtnLoader();
hideBtnLoader();
}
And here is an example of how I would like to use it.
import btnLoader from 'loaderPlugin';
const signupBtn = document.getElementById('signup-btn');
signupBtn.addEventListener('click', function(e) {
e.preventDefault();
btnLoader.showBtnLoader(signupBtn, 'Validating');
// Call API here
});
// Following API response
hideBtnLoader(signupBtn);
The issue I have is that I want to store the originalBtnText from the showBtnLoader function and then use that variable in the hideBtnLoader function. I could of course achieve this in a different way (such as adding the value as a data attribute and grabbing it later) but I wondered if there is a simple way.
Another issue I have is that I don't know the correct way of calling each individual function and whether I am importing it correctly. I have tried the following.
btnLoader.showBtnLoader(signupBtn, 'Validating');
btnLoader(showBtnLoader(signupBtn, 'Validating'));
showBtnLoader(signupBtn, 'Validating');
But I get the following error:
Uncaught ReferenceError: showBtnLoader is not defined
at HTMLButtonElement.<anonymous>
I have read some good articles and SO answers such as http://2ality.com/2014/09/es6-modules-final.html and ES6 export default with multiple functions referring to each other but I'm slightly confused as to the 'correct' way of doing this to make it reusable.
Any pointers would be much appreciated.
I would export a function that creates an object with both show and hide functions, like this:
export default function(btn, loadingText) {
function show() {
const clickedBtn = btn;
const spinner = document.createElement('div');
spinner.classList.add('spin-loader');
originalBtnText = clickedBtn.textContent;
clickedBtn.textContent = loadingText;
clickedBtn.appendChild(spinner);
clickedBtn.setAttribute('disabled', true);
clickedBtn.classList.add('loading');
}
function hide() {
const clickedBtn = btn.target;
clickedBtn.textContent = originalBtnText;
clickedBtn.removeAttribute('disabled');
clickedBtn.classList.remove('loading');
}
return {
show,
hide,
};
}
Then, to use it:
import btnLoader from 'btnloader';
const signupBtn = document.getElementById('signup-btn');
const signupLoader = btnLoader( signupBtn, 'Validating' );
signupBtn.addEventListener('click', function(e) {
e.preventDefault();
signupLoader.show();
// Call API here
});
// Following API response
signupLoader.hide();
If you need to hide it from a different file from where you showed it, then you can export the instance:
export const signupLoader = btnLoader( signupBtn, 'Validating' );
And later import it.
import { signupLoader } from 'signupform';
function handleApi() {
signupLoader.hide();
}
Youre maybe overriding the Element.prototype, to make it accessible right from that element. However, i wouldnt set values onto that element, i would rather return an object with all the neccessary stuff:
export function implementBtnLoader(){
Element.prototype.showBtnLoader=function( loadingText) {
const clickedBtn = this;
const spinner = document.createElement('div');
spinner.classList.add('spin-loader');
var originalBtnText = clickedBtn.textContent;
clickedBtn.textContent = loadingText;
clickedBtn.appendChild(spinner);
clickedBtn.setAttribute('disabled', true);
clickedBtn.classList.add('loading');
return {
text:originalBtnText,
el:this,
hideBtnLoader: function() {
const clickedBtn = this.target;
clickedBtn.textContent = this.text;
clickedBtn.removeAttribute('disabled');
clickedBtn.classList.remove('loading');
return this;
}
};
};
}
export function btnLoader() {
implementBtnLoader();
}
When imported, and implementBtnLoader was called, one can do:
var loader=document.getElementById("test").showBtnLoader();
console.log(loader.text);
loader.hideBtnLoader();

Multiple browsers and the Page Object pattern

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();

Categories

Resources