Make a 'common' login.js include; with nightwatch.js tests - javascript

When writing tests for my web app; I have to first simulate login before the rest of my tests can run and see inner pages. Right now I'm working on modulating the code, so that way I can just make an 'include' for the common function; such as my login. But as soon as I move the below code in a separate file, and call the include via require - it no longer runs as expected.
ie. the below logs in and allows my other functions, if, included in the same file. above my other inner screen functions.
// Login screen, create opportunity
this.LoginScreen = function(browser) {
browser
.url(Data.urls.home)
.waitForElementVisible('#login', 2000, false)
.click('#login')
.waitForElementVisible('div.side-panel.open', 4000, false)
.waitForElementVisible('input#email', 2000, false)
.waitForElementVisible('input#password', 2000, false)
.click('input#email')
.pause(500)
.setValue('input#email', Data.ProjMan.username)
.click('input#password')
.pause(500)
.setValue('input#password', Data.ProjMan.password)
.click('input#email')
.pause(500)
.click('div.form.login-form .btn')
.pause(5000)
Errors.checkForErrors(browser);
};
// Inner functions run after here, sequentially
But as soon as I move the above in a separate file, for instance; Logins.js, then call it at the top of the original test file with. (yes, correct path).
var Logins = require("../../lib/Logins.js");
It just doesn't simulate the login anymore. Any thoughts? Should I remove the this.LoginScreen function wrapper, and call it differently to execute from the external file, or do I need to fire it from the original file again, aside from the external require path?
I have also tried wrapping 'module.exports = {' around the login function from separate file, but still failing.

Nightwatch allows you to run your Page object based tests i.e you can externalize your common test functions and use them in your regular tests. This can be achieved using 'page_objects_path' property. I have added the common 'login' functionality and used it in sample 'single test' in the project here.
Working:
Place your common function in .js file and place it under a folder(ex: tests/pages/login.js) and pass the folder path in nighwatch config file as below:
nightwatch_config = {
src_folders : [ 'tests/single' ],
page_objects_path: ['tests/pages'],
Below is an example of common login function (login.js):
var loginCommands = {
login: function() {
return this.waitForElementVisible('body', 1000)
.verify.visible('#userName')
.verify.visible('#password')
.verify.visible('#submit')
.setValue('#userName', 'Enter Github user name')
.setValue('#password', 'Enter Github password')
.waitForElementVisible('body', 2000)
}
};
module.exports = {
commands: [loginCommands],
url: function() {
return 'https://github.com/login';
},
elements: {
userName: {
selector: '//input[#name=\'login\']',
locateStrategy: 'xpath'
},
password: {
selector: '//input[#name=\'password\']',
locateStrategy: 'xpath'
},
submit: {
selector: '//input[#name=\'commit\']',
locateStrategy: 'xpath'
}
}
};
Now, in your regular test file, create an object for the common function as below and use it.
module.exports = {
'Github login Functionality' : function (browser) {
//create an object for login
var login = browser.page.login();
//execute the login method from //tests/pages/login.js file
login.navigate().login();
//You can continue with your tests below:
// Also, you can use similar Page objects to increase reusability
browser
.pause(3000)
.end();
}
};

The above answer is absolutly correct however I did struggle with how to supply login user details.
This is what I ended up using:
var loginCommands = {
login: function() {
return this.waitForElementVisible('body', 1000)
.setValue("#email", "<some rnd email address>")
.setValue('#password', "<some rnd password>")
.click('button[type=submit]')
.pause(1000)
}
};
module.exports = {
commands: [loginCommands],
url: function() {
return 'https://example.com/login';
}
};
This can be used in the same way as the accepted answer just posting for others who come searching.

Related

programmically give input to yeoman cmd for CI/CD tool

when we run the yeoman it is asking to add input one by one, is there anyway to give all input one go ? or programically?
for Example :
yo azuresfguest
it is asking to add 5 inputs, which i want to give one time, so i can run in CI/CD system
Thanks,
There isn't a general way, unfortunately. The specific generator would need to allow it.
I think it is deserving of a feature request on the Yeoman project, which I've logged here.
As a cumbersome workaround, you can create your own generator which re-uses an existing generator. The TypeScript code below gives an example; I'm using this approach to automate my CI process.
Add option to constructor:
constructor(args: string, opts: Generator.GeneratorOptions) {
super(args, opts);
...
this.option("prompts-json-file", {
type: String,
default: undefined,
description: "Skips prompting; uses file contents. Useful for automation",
});
}
Use the option:
async prompting() {
if (this.options["prompts-json-file"] !== undefined) {
this.answers = new Answers(JSON.parse(
fs.readFileSync(this.options["prompts-json-file"]).toString()
));
}
else {
this.answers = ...
}
}
Unfortunately this does bypass prompt validation so you'd need to separately ensure your file contains valid values.
Using it is relatively simple:
yo my-generator --prompts-json-file ./prompts.json
This should be accomplished using Yeomans storage API and a .yo-rc.json file:
https://yeoman.io/authoring/storage.html
I used to use a self defined option to make that optional similar to the approach from #BjornO
constructor(args, opts) {
super(args, opts);
// This method adds support for a `--yo-rc` flag
this.option('yo-rc', {
desc: 'Read and apply options from .yo-rc.json and skip prompting',
type: Boolean,
defaults: false
});
}
initializing() {
this.skipPrompts = false;
if (this.options['yo-rc']) {
const config = this.config.getAll();
this.log(
'Read and applied the following config from ' +
chalk.yellow('.yo-rc.json:\n')
);
this.log(config);
this.log('\n');
this.templateProps = {
projectName: config.projectName,
};
this.skipPrompts = true;
}
}
prompting() {
if (!this.skipPrompts) {
// Have Yeoman greet the user.
this.log(
yosay(
`Yo, welcome to the ${superb()} ${chalk.yellow(
'Baumeister'
)} generator!`
)
);
const prompts = [
{
type: 'input',
name: 'projectName',
message: 'What’s the name of your project?',
// Default to current folder name
default: _s.titleize(this.appname)
}
];
return this.prompt(prompts).then(props => {
this.templateProps = {
projectName: props.projectName
};
});
}
}
See https://github.com/micromata/generator-baumeister/blob/master/app/index.js#L20-L69 for the whole generator code and https://github.com/micromata/generator-baumeister/blob/master/\_\_tests__/yo-rc.json for the corresponding .yo-rc.json file.

How to add nested functions javascript in custom commands for Nightwatch testing- forEach -loop through elements

Hi I am new to javascript and Nightwatch, I am a manual tester who started doing automation about 6 months ago.
I am writing test cases for checking the details of a product, with collapsible menus. Pressing + button will open and display a list of elements, when closing with the same button, it closes the list, and shows a counter with the number of items on the list.
I have a function that is correctly doing this procedure, but I have it written on the test. I would like to use it in the Page where I have all elements and functions related to that page. And I would like to call that function from the test. I have been able to do this, but not with cases with nested functions, because I do not know how to write it.
These are my pages:
loginPage.js;
productPage.js;
productFuntionalityListPage.js;
This is my test:
module.exports = {
'Buy a Product with Bank Account': function (browser) {
const login = browser.page.loginPage();
const productList = browser.page.productPage();
const productFunctionalityList = browser.page.productFuntionalityListPage();
login
.navigate()
.checkLoginPage();
productList
.getAProduct()
//------------------------------------------Features--------------------------------------
//function to click on each button for functionalities and wait for list to appear
function displayFunctionsList(elems) {
elems.value.forEach(function (element) {
browser.elementIdClick(element.ELEMENT)
//wait for list to appear
.waitForElementVisible('.list_of_items')
.pause(2000)
})
}
// click on each function and wait for list to appear
browser.elements('css selector', '.expand_collapse_btn', displayFunctionsList, 5000)
browser.useCss()
// close each function
function closeFunctionsList(elems) {
elems.value.forEach(function (element) {
browser.elementIdClick(element.ELEMENT)
//after click close wait for count to appear
.waitForElementVisible("input[data-id='counter']")
.pause(2000)
})
}
browser.elements('css selector', '.expand_collapse_btn', closeFunctionsList, 2000)
browser.end()
}
}
This is working correctly.
Below it's what I have tried and does not work:
Page:
productFuntionalityListPage.js
module.exports = {
elements: {
counterOfItemsInList: {
locatorStrategy: 'css selector'
selector: "input[data-id='counter']",
},
expandCollapseBtn: {
locateStrategy: 'css selector',
selector: '.expand_collapse_btn',
},
listOfItems: {
locateStrategy: 'css selector',
selector: '.list_of_items',
}
},
commands: [{
displayFunctionsList: function () {
function displayFunctionsList(elems) {
elems.value.forEach(function (element) {
this.elementIdClick(element.ELEMENT)
//wait for list to appear
.waitForElementVisible('#listOfItems')
.pause(2000)
})
}
this.elements('css selector', '#expandCollapseBtn', displayFunctionsList, 5000)
},
closeFunctionsList: function () {
function closeFunctionsList(elems) {
elems.value.forEach(function (element) {
this.elementIdClick(element.ELEMENT)
//wait for list to appear
.waitForElementVisible('#counterOfItemsInList')
.pause(2000)
})
}
this.elements('css selector', '#expandCollapseBtn', closeFunctionsList, 5000)
}
}]
}
Test calling function from page:
module.exports = {
'Buy a Product with Bank Account': function (browser) {
const login = browser.page.loginPage();
const productList = browser.page.productPage();
const productFunctionalityList = browser.page.productFuntionalityListPage();
login
.navigate()
.checkLoginPage();
productList
.getAProduct()
//------------------------------------------Features--------------------------------------
//calling displayFunctionsList from productFuntionalityListPage.js
productFunctionalityList.displayFunctionsList()
//calling closeFunctionsList from productFuntionalityListPage.js
productFunctionalityList.closeFunctionsList()
browser.end()
}
}
Result after running the test above:
Error:
TypeError: this.elements is not a function
- writing an ES6 async test case? - keep in mind that commands return a Promise;
- writing unit tests? - make sure to specify "unit_tests_mode=true" in your config.
Could anyone please help me adding these functions as custom commands in the productFuntionalityListPage.js and call these functions from the test itself? Not sure what's wrong, because of my lack of javascript and nightwatch knowledge.
Try passing browser as a variable when calling the function like this -
##Test page##
//Example call
gmail.selectEmail(browser, 'browser authentication')
And then the method in the pageObject -
##Page Object##
//Example Method
selectEmail(browser, searchValue){
browser.blah(searchValue);
browser.blah
browser.blah
};
Its slightly messy way of getting it to work but this has saved my bacon a few times

Using Google One Tap in Angular

I'd like to use Google One Tap in my Angular 11 app. Following the documentation I added <script async defer src="https://accounts.google.com/gsi/client"></script> to my html and then used the following code in my app.component.html:
<div id="g_id_onload"
data-client_id="MY_GOOGLE_CLIENT_ID"
data-callback="handleCredentialResponse",
data-cancel_on_tap_outside="false">
</div>
The popup works fine, though I can't seem to log in. If I create a function handleCredentialResponse in app.component.ts, I get the following error: [GSI_LOGGER]: The value of 'callback' is not a function. Configuration ignored.
If I instead try to use the JavaScript API, Typescript throws the following error: Property 'accounts' does not exist on type 'typeof google'
What should I do to be able to using Google One Tap in Angular?
I had a similar problem when I used the HTML API approach, so I ended up using the JavaScript API instead.
Here's what I did:
First, make sure to install the #types/google-one-tap package.
As you mentioned, I'm also importing the script in my index.html file, like so:
<body>
<script src="https://accounts.google.com/gsi/client" async defer></script>
<app-root></app-root>
</body>
Now, moving on to your main component which in my case is app.component.ts, import the following first:
import { CredentialResponse, PromptMomentNotification } from 'google-one-tap';
Then, you can add this on the ngOnInit(). Make sure to read the documentation to get more details on the onGoogleLibraryLoad event:
// #ts-ignore
window.onGoogleLibraryLoad = () => {
console.log('Google\'s One-tap sign in script loaded!');
// #ts-ignore
google.accounts.id.initialize({
// Ref: https://developers.google.com/identity/gsi/web/reference/js-reference#IdConfiguration
client_id: 'XXXXXXXX',
callback: this.handleCredentialResponse.bind(this), // Whatever function you want to trigger...
auto_select: true,
cancel_on_tap_outside: false
});
// OPTIONAL: In my case I want to redirect the user to an specific path.
// #ts-ignore
google.accounts.id.prompt((notification: PromptMomentNotification) => {
console.log('Google prompt event triggered...');
if (notification.getDismissedReason() === 'credential_returned') {
this.ngZone.run(() => {
this.router.navigate(['myapp/somewhere'], { replaceUrl: true });
console.log('Welcome back!');
});
}
});
};
Then, the handleCredentialResponse function is where you handle the actual response with the user's credential. In my case, I wanted to decode it first. Check this out to get more details on how the credential looks once it has been decoded: https://developers.google.com/identity/gsi/web/reference/js-reference#credential
handleCredentialResponse(response: CredentialResponse) {
// Decoding JWT token...
let decodedToken: any | null = null;
try {
decodedToken = JSON.parse(atob(response?.credential.split('.')[1]));
} catch (e) {
console.error('Error while trying to decode token', e);
}
console.log('decodedToken', decodedToken);
}
I too had the same problem in adding the function to the angular component.
Then i found a solution by adding JS function in appComponent like this:
(window as any).handleCredentialResponse = (response) => {
/* your code here for handling response.credential */
}
Hope this help!
set the div in template to be rendered in ngOnInit
`<div id="loginBtn" > </div>`
dynamically inject script tag in your login.ts as follows
constructor(private _renderer2: Renderer2, #Inject(DOCUMENT) private _document: Document){}
ngAfterViewInit() {
const script1 = this._renderer2.createElement('script');
script1.src = `https://accounts.google.com/gsi/client`;
script1.async = `true`;
script1.defer = `true`;
this._renderer2.appendChild(this._document.body, script1);
}
ngOnInit(): void {
// #ts-ignore
window.onGoogleLibraryLoad = () => {
// #ts-ignore
google.accounts.id.initialize({
client_id: '335422918527-fd2d9vpim8fpvbcgbv19aiv98hjmo7c5.apps.googleusercontent.com',
callback: this.googleResponse.bind(this),
auto_select: false,
cancel_on_tap_outside: true,
})
// #ts-ignore
google.accounts!.id.renderButton( document!.getElementById('loginBtn')!, { theme: 'outline', size: 'large', width: 200 } )
// #ts-ignore
google.accounts.id.prompt();
}
}
async googleResponse(response: google.CredentialResponse) {
// your logic goes here
}
Google One Tap js library tries to find callback in the global scope and can't find it, because your callback function is scoped somewhere inside of your app, so you can attach your callback to window, like window.callback = function(data) {...}.
Also, since you are attaching it to window, it's better to give the function a less generic name.

Jest: Trying to mock nested functions and redirects in jest, but no avail

We are using a javascript framework(Not react) to render the ui.
main.js
function logout(){
someObj.lock($('#container'), 'Logging out', true);
document.location = app.context + `/${appName}/signout.action?name=${appName}`;
}
function action(event){
switch(event.target){
case 'user:logout':
logout();
break;
case 'user:application':
document.location = app.context + "/";
break;
}
}
module.exports = {
action: action,
logout: logout
}
main.js along with another js file renders a navbar and a dropdown. My intention is to check whether title, dropdown in the navbar is rendered. Also I
am testing whether the browser redirect takes place in the right way.
action method takes an event object and based on its type, either performs signout('user:logout') or redirects to application page('user:application').
tests/main.js
import main from '../main';
describe("some title", () => {
it("some behavior", () => {
let event = {
target: 'user:logout'
}
let app = {
context: ''
}
let appName = 'Some app';
main.logout = jest.fn();
someObj = jest.fn();
someObj.lock = jest.fn();
document.location.assign = jest.fn();
main.action(event);
expect(main.logout).toHaveBeenCalled();
expect(document.location.assign).toBeCalledWith(app.context + `/${appName}/signout.action?name=${appName}`);
})
});
In the test file, I am trying to mock logout function. However it is executing someObj.lock function. someObj is not availabe to tests/main.js
and I am mocking it as well. I'm not sure whether I have to use spyOn instead. I'm using document.location.assign to test for browser redirects.
This test is not working and the terminal is displaying TypeError: Could not parse "/application name/signout.action?name=application name" as URL.
I have spent an entire day testing this feature but to no avail. I need some advice on the best way to test this feature.
Links explored: Intercept navigation change with jest.js (or how to override and restore location.href)
jest documentation

Meteor: Security in Templates and Iron Router

I'm enjoying working with Meteor and trying out new things, but I often try to keep security in mind. So while I'm building out a prototype app, I'm trying to find the best practices for keeping the app secure. One thing I keep coming across is restricting a user based on either a roll, or whether or not they're logged in. Here are two examples of issues I'm having.
// First example, trying to only fire an event if the user is an admin
// This is using the alaning:roles package
Template.homeIndex.events({
"click .someclass": function(event) {
if (Roles.userIsInRole(Meteor.user(), 'admin', 'admin-group') {
// Do something only if an admin in admin-group
}
});
My problem with the above is I can override this by typing:
Roles.userIsInRole = function() { return true; } in this console. Ouch.
The second example is using Iron Router. Here I want to allow a user to the "/chat" route only if they're logged in.
Router.route("/chat", {
name: 'chatHome',
onBeforeAction: function() {
// Not secure! Meteor.user = function() { return true; } in the console.
if (!Meteor.user()) {
return this.redirect('homeIndex');
} else {
this.next();
}
},
waitOn: function () {
if (!!Meteor.user()) {
return Meteor.subscribe("messages");
}
},
data: function () {
return {
chatActive: true
}
}
});
Again I run into the same problem. Meteor.user = function() { return true; } in this console blows this pattern up. The only way around this I have found thus far is using a Meteor.method call, which seems improper, as they are stubs that require callbacks.
What is the proper way to address this issue?
Edit:
Using a Meteor.call callback doesn't work for me since it's calling for a response asynchronously. It's moving out of the hook before it can handle the response.
onBeforeAction: function() {
var self = this;
Meteor.call('someBooleanFunc', function(err, res) {
if (!res) {
return self.redirect('homeIndex');
} else {
self.next();
}
})
},
I guess you should try adding a check in the publish method in server.
Something like this:
Meteor.publish('messages') {
if (Roles.userIsInRole(this.userId, 'admin', 'admin-group')) {
return Meteor.messages.find();
}
else {
// user not authorized. do not publish messages
this.stop();
return;
}
});
You may do a similar check in your call methods in server.

Categories

Resources