Angular2+ e2e testing - Cannot use by.id - javascript

I'm new to Angular2 and haven't developed the Angular components to be tested. However, I'm supposed to write some some UI (e2e) tests but I'm not even able to input text in an input field.
My problem is that
element(by.id('username')).sendKeys('test')
is not working. (Same with Button elements and so on)
I'm sure that it is only a small thing but I'm not able to find out what it is.
I have the following configuration:
Proctractor: 5.1.2
chrome driver: 58.0.3029.110
OS: Windows NT 6.1.7601 SP1 x86_64
Spec file:
import { LoginPage } from './login.po';
describe('login tests', function() {
let page: LoginPage;
beforeEach(() => {
page = new LoginPage();
});
it('Demo', () => {
page.navigateTo();
page.getUsernameInput().sendKeys('test');
});
});
Page Object file:
import { browser, element, by } from 'protractor';
export class LoginPage {
navigateTo() {
return browser.get('/login');
}
getParagraphText() {
return element(by.class('app-root h1')).getText();
}
getUsernameInput() {
return element(by.id('username'));
}
}
The HTML template:
....
<div>
<input
id="username"
name="username"
ngModel
type="text"
placeholder="{{something}}"
autocomplete="off"
class="input-text"
required>
</div>
...
Proctractor config
var SpecReporter = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 120000,
getPageTimeout: 120000,
specs: [
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
seleniumAddress: 'http://localhost:4444/wd/hub',
baseUrl: 'http://localhost:8001',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
useAllAngular2AppRoots: true,
beforeLaunch: function() {
require('ts-node').register({
project: 'e2e'
});
},
onPrepare: function() {
jasmine.getEnv().addReporter(new SpecReporter());
}
};
Any help is highly appreciated.
EDIT:
None of the solutions worked in my case. I ended up using browser.driver.findElement(by.id('username')); instead of element(by.id('username')); This is unsatisfying because I still don't understand why this doesn't work. I'd be thankful if someone could give me a hint or explanation.

I think your problem is timing.
What happens if you do:
it('Demo', () => {
// wait for page to change to /login
return page.navigateTo().then(() => {
// then look for user input and write 'test'
return page.getUsernameInput().sendKeys('test');
});
});
Edit:
Sounds odd to me that browser.driver.findElement(by.id('username')) works since element(by.id('username')) should be equivalent.
I use a helper class for a lot of the browser interactions, perhaps worth a shot.
Snippets I use for finding element and sending keystrokes:
public static async getElement(locator: By | Function, waitMs?: number): Promise<ElementFinder | any> {
await BrowserHelper.waitForVisibilityOf(locator, waitMs | 1000);
return element(locator);
}
public static sendKeys(locator: By | Function, keys: string, clear?: boolean, waitMs?: number): Promise<void> {
return BrowserHelper.getElement(locator, waitMs).then((element: ElementFinder) => {
if (!clear) {
return element;
}
return element.clear().then(() => element);
}).then((element: ElementFinder) => {
return element.sendKeys(keys)
});
}
public static async waitForVisibilityOf(locator: By | Function, waitMs?: number): Promise<any> {
await browser.wait(EC.presenceOf(element(locator)), waitMs || 5000).then(() => {
// visible
}, (error) => {
console.error('timeout at waitForVisibilityOf', locator, error);
});
}

I believe this is due to your getUsernameInput() method returning not the locator in this case. As per Protractor documentation,
The element() function returns an ElementFinder object. The ElementFinder knows how to locate the DOM element using the locator you passed in as a parameter, but it has not actually done so yet. It will not contact the browser until an action method has been called.
You can try this modified code
getUsernameInput() {
element(by.id('username')).sendKeys('text');
}
}
and then using
it('Demo', () => {
page.navigateTo();
page.getUsernameInput();
});
});
Also, I'm not sure your getText() would return the text, because getText() returns a Promise, which you would have to resolve. This has been explained here.

Related

How to properly add a class inside Cypress code

I am learning Cypress along with JavaScript. I am running into a problem that I am not certain how to search it into documentation. The site I started testing has the typical wait issues so I encountered a very good solution here.
Now my test is looking in this way
/// <reference types="Cypress" />
let appHasStarted
function spyOnAddEventListener (win) {
// win = window object in our application
const addListener = win.EventTarget.prototype.addEventListener
win.EventTarget.prototype.addEventListener = function (name) {
if (name === 'change') {
// web app added an event listener to the input box -
// that means the web application has started
appHasStarted = true
// restore the original event listener
win.EventTarget.prototype.addEventListener = addListener
}
return addListener.apply(this, arguments)
}
}
function waitForAppStart() {
// keeps rechecking "appHasStarted" variable
return new Cypress.Promise((resolve, reject) => {
const isReady = () => {
if (appHasStarted) {
return resolve()
}
setTimeout(isReady, 0)
}
isReady()
})
}
describe('Main test suite', () => {
beforeEach(() => {
cy.visit('http://mercadolibre.com.ar',{
onBeforeLoad: spyOnAddEventListener
}).then({ timeout: 10000 }, waitForAppStart)
})
it('search first scanner', () => {
cy.contains('nav-search-input').type("scanner bluetooth para auto")
})
})
The problem with this is, I should replicate spyOnAddEventListener, waitForAppStart and variable appHasStarted at the beginning of every source file but I want to avoid this. How could properly extend this functions as a part of the internal source project without replicating in every test source? I have tried to make a simple source JavaScript file at the root of the project but when I import it, Cypress clients give an unrelated plug error like this one:
It looks like you've added the code to /cypress/plugins/index.js, but that is for task extensions (code that requires NodeJS access).
The two functions can be added to a file, ideally in the /cypress/support folder
wait-for-app-utils.js
let appHasStarted
function spyOnAddEventListener (win) {
...
}
function waitForAppStart() {
...
}
module.exports = {
spyOnAddEventListener,
waitForAppStart
}
test
import {spyOnAddEventListener, waitForAppStart} from '../support/wait-for-app-utils.js'
describe('Main test suite', () => {
beforeEach(() => {
cy.visit('http://mercadolibre.com.ar', {
onBeforeLoad: spyOnAddEventListener
}).then({ timeout: 10000 }, waitForAppStart)
})
Another approach is to wrap it all up (including the visit) into a custom command. Now there's no need to export and import, the command will be available globally.
/cypress/support/commands.js
let appHasStarted
function spyOnAddEventListener (win) {
...
}
function waitForAppStart() {
...
}
Cypress.Commands.add('visitAndWait', (url) =>
cy.visit(url, { onBeforeLoad: spyOnAddEventListener })
.then({ timeout: 10000 }, waitForAppStart)
)
test
describe('Main test suite', () => {
beforeEach(() => {
cy.visitAndWait('http://mercadolibre.com.ar')
})

How to fix the Error "TypeError: cy.[custom command] is not a function"?

I have written some function in commands.js file for cypress automation testing, out of which I am able to invoke only one i.e."login" but unable to invoke other functions form another .js file. Cypress Test Runner showing
"TypeError: cy.FillAddCaseDetails is not a function"
describe('Adding a Case on CSS Poratal ', function() {
before(function () {
cy.login() // calling login function successfully
})
it('open add case',function(){
cy.wait(9000)
cy.hash().should('contains','#/home')
cy.wait(['#GETcontentLoad']);
cy.wait(['#POSTcontentLoad']);
cy.get('[uib-tooltip="Add Case"]').click({force:true})
cy.log('clicked on Add case')
cy.wait(3000)
cy.get('[ng-click="lookup.cancel()"]').click({force: true})
cy.get('[ng-click="lookup.closeAddCase()"]').click({force: true})
cy.get('[uib-tooltip="Add Case"]').click({force:true})
cy.wait(3000)
cy.get('[ng-model="lookup.selectedPartner"]',{force:true})
.type(AddJob.JobData.Partner,{force: true})
cy.xpath('//input[#ng-model="lookup.selectedPartner"]')
.should('be.visible').then(() => {
cy.FillAddCaseDetails() // unable to call
cy.FillCustomerDetails() // unable to call
})
Function:
Cypress.Commands.add("FillCustomerDetails", () => {
cy.get('[ng-model="lookup.firstName"]')
.type(AddJob.JobData.FirstName, { force: true})
cy.get('[ng-model="lookup.lastName"]')
.type(AddJob.JobData.LastName, { force: true })
cy.get('[ng-model="lookup.customerPhone"]')
.type(AddJob.JobData.CustomerPhone, { force: true })
cy.get('[value="NEXT"]').click({ force: true })
})
expected : function will get called
actual : TypeError: cy.FillAddCaseDetails is not a function
This is the top result for this error so I would like to add what I did to fix it. This is relevant to version >=10 and typescript. The problem ended up being that the supportFile setting in cypress.config.ts was set to false; I changed my config to this:
import cypress, { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
'baseUrl': 'http://localhost:4200',
supportFile: 'cypress/support/e2e.ts'
},
})
I created the custom commands in commands.ts
declare namespace Cypress {
interface Chainable<Subject = any> {
/**
* Custom command to select DOM element by data-cy attribute.
* #example cy.dataCy('greeting')
*/
clearIndexedDB(): Promise<void>
}
}
Cypress.Commands.add('clearIndexedDB', async () => {
const databases = await window.indexedDB.databases();
await Promise.all(
databases.map(
({ name }) => {
if (!name) return
return new Promise((resolve, reject) => {
const request = window.indexedDB.deleteDatabase(name);
request.addEventListener('success', resolve);
request.addEventListener('blocked', resolve);
request.addEventListener('error', reject);
})
},
),
);
});
Then I uncommented this line in my e2e.ts file
import './commands';
In my case solution was a restart of the cypress test runner.
If you added your Custom Command to support/commands.js file, You need to import that file from support/index.js file. Create support/index.js, if it's not available and add the line import "./commands.js" to it.
From the Cypress docs: https://on.cypress.io/typescript#Types-for-custom-commands
if you add the command cy.dataCy into your supportFile like this:
// cypress/support/index.js
Cypress.Commands.add('dataCy', (value) => {
return cy.get(`[data-cy=${value}]`)
})
Then you can add the dataCy command to the global Cypress Chainable interface (so called because commands are chained together) by creating a new TypeScript definitions file beside your supportFile, in this case at cypress/support/index.d.ts.
// in cypress/support/index.d.ts
// load type definitions that come with Cypress module
/// <reference types="cypress" />
declare namespace Cypress {
interface Chainable {
/**
* Custom command to select DOM element by data-cy attribute.
* #example cy.dataCy('greeting')
*/
dataCy(value: string): Chainable<Element>
}
}
cy.xpath("//div[#class='c-navigatorItem-faceplate ng-scope ng-isolate-scope']").click();
Is it a valid to use because I am getting the TypeError cy.xpath is not a function

How to design Protractor page object navigation to avoid "window.angular is undefined" errors

I am having trouble with "window.angular is undefined" errors and I'm sure it has something to do with the asynchronous execution of JavaScript, but I don't know how to get around that. The login page and the initial landing page are non-Angular pages and the rest of the application is Angular. So, I need to login using a non-Angular page, then once the non-Angular landing page loads, open a drop down menu and click a link that loads an Angular page. It seems that all the actions just fly on, none of them waiting for navigation to complete before checking whether Angular is loaded or not.
I have this base class for page objects:
export class AbstractLoadable {
constructor(isAngularComponent) {
this.isAngularComponent = isAngularComponent;
}
initComponent() {
console.log("Initializing: " + this.isAngularComponent);
browser.waitForAngularEnabled(this.isAngularComponent);
if(this.isAngularComponent) {
console.log("Waiting for angular");
browser.waitForAngular();
}
return this;
}
}
I have this login page:
import {AbstractLoadable} from "./AbstractLoadable";
import {HomePage} from "./HomePage";
export class LoginPage extends AbstractLoadable {
constructor() {
super(false);
this.usernameInput = element(by.id("username"));
this.passwordInput = element(by.id("password"));
this.loginButton = element(by.css("[name='login']"));
}
load(baseUrl) {
browser.driver.get(baseUrl);
return this.initComponent();
}
login(username, password) {
this.usernameInput.sendKeys(username);
this.passwordInput.sendKeys(password);
this.loginButton.click();
return new HomePage().initComponent();
}
}
I have this Home page:
import {AbstractLoadable} from "./AbstractLoadable";
import {LoginPage} from "./LoginPage";
import {AngularPage} from "./AngularPage";
import {ExtendedExpectedConditions} from "../ExtendedExpectedConditions";
export class HomePage extends AbstractLoadable {
constructor() {
super(false);
this.menuButton = element(by.id("some locator"));
this.menuContainer = element(by.css("some locator"));
this.menuOptionLink = element(by.css("some locator"));
}
isMenuButtonPresent() {
return ExtendedExpectedConditions.isElementPresent(this.menuButton);
}
isMenuExpanded() {
return ExtendedExpectedConditions.isElementDisplayed(this.menuContainer);
}
expandMenu() {
this.isMenuButtonPresent().then(isPresent => {
if(!isPresent) {
ExtendedExpectedConditions.waitForElementVisible(this.menuButton, 120000)
}
});
this.isMenuExpanded().then(isExpanded => {
if(!isExpanded) {
this.menuButton.click();
ExtendedExpectedConditions.waitForElementVisible(this.menuContainer);
}
});
}
loadAngularPage() {
this.expandMenu();
this.menuOptionLink.click();
return new AngularPage().initComponent();
}
}
The wait methods are static utility methods in this class:
export class ExtendedExpectedConditions {
static waitForElementPresent(element, timeout = 30000) {
browser.wait(ExpectedConditions.presenceOf(element), timeout);
}
static waitForElementVisible(element, timeout = 30000) {
browser.wait(ExpectedConditions.visibilityOf(element), timeout);
}
static isElementPresent(element) {
return element.isPresent()
}
}
The angular page class has this constructor which passes 'true' to the base class constructor, indicating that it is an Angular page:
import {AbstractLoadable} from "./AbstractLoadable";
export class AngularPage extends AbstractLoadable {
constructor() {
super(true);
this.loadDialogButton = element(by.css("some locator"));
}
loadDialog() {
this.loadDialogButton.click();
//This class also extends the base class and has a constructor that passes true to the base class constructor, indicating that it is an Angular component
return new AngularDialog().initComponent();
}
}
When I tried to execute this test, I keep getting "window.angular is undefined" errors:
import {LoginPage} from "../pageobject/LoginPage.js";
describe("Test", () => {
it("Login and navigate", () => {
let hp = new LoginPage().load(browser.baseUrl).login("user", "pass");
let ap = hp.loadAngularPage();
let dialog = ap.loadDialog();
//isLoaded() checks visibility of dialog container element
expect(dialog.isLoaded()).toBeTruthy();
});
});
The console output is this:
Initializing: false
Initializing: false
Initializing: true
Waiting for angular
Initializing: true
Waiting for angular
Failed: Error while waiting for Protractor to sync with the page: "window.angular is undefined. This could be either because this is a non-angular page or because your test involves client-side navigation, which can interfere with Protractor's bootstrapping. See http://git.io/v4gXM for details"
Error: Error while waiting for Protractor to sync with the page: "window.angular is undefined. This could be either because this is a non-angular page or because your test involves client-side navigation, which can interfere with Protractor's bootstrapping. See http://git.io/v4gXM for details"
My package.json is this:
{
"name": "ui-tests",
"version": "1.0.0",
"description": "A description",
"scripts": {
"test": "node_modules/protractor/bin/protractor conf.js",
"start_selenium": "node_modules/protractor/node_modules/webdriver-manager/bin/webdriver-manager start",
"update_selenium": "node_modules/protractor/node_modules/webdriver-manager/bin/webdriver-manager update"
},
"dependencies": {
"babel-preset-es2015": "^6.24.1",
"babel-register": "^6.24.1",
"jasmine-reporters": "^2.2.1",
"protractor": "^5.1.2"
},
"keywords": [
"es6"
],
"babel": {
"presets": [
"es2015"
]
}
}
My conf.js is this:
require("babel-register");
exports.config = {
framework: 'jasmine2',
rootElement: 'body',
seleniumServerJar:'./node_modules/protractor/node_modules/webdriver-manager/selenium/selenium-server-standalone-3.4.0.jar',
chromeDriver: './node_modules/protractor/node_modules/webdriver-manager/selenium/chromedriver_2.30',
specs: ['tests/*Spec.js'],
capabilities: {
browserName: 'chrome',
acceptSslCerts: true,
trustAllSSLCertificates: true,
chromeOptions: {
args: ['--no-sandbox']
},
},
baseUrl: 'https://www.myurl.com',
suites: {
login: '../tests/theTestSpec.js'
},
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 3600000,
isVerbose: true
},
getPageTimeout: 120000,
allScriptsTimeout: 3600000,
delayBrowserTimeInSeconds: 0,
onPrepare: function() {
require("babel-register");
let origFn = browser.driver.controlFlow().execute;
browser.driver.controlFlow().execute = function () {
let args = arguments;
origFn.call(browser.driver.controlFlow(), function () {
return protractor.promise.delayed(this.delayBrowserTimeInSeconds * 100);
});
return origFn.apply(browser.driver.controlFlow(), args);
};
let getScreenSize = function() {
return browser.driver.executeScript(function() {
return {
width: window.screen.availWidth,
height: window.screen.availHeight
};
});
};
getScreenSize().then(function(screenSize) {
browser.driver.manage().window().setSize(screenSize.width, screenSize.height);
});
}
};
All protractor calls return promises. In your current code you create them but do not always wait for them to resolve. Everything that needs to happen after each other needs to be chained with the then keyword. promises
for example both of these checks are being done at the same time.
expandMenu() {
this.isMenuButtonPresent().then(isPresent => {
if(!isPresent) {
ExtendedExpectedConditions.waitForElementVisible(this.menuButton, 120000)
}
});
this.isMenuExpanded().then(isExpanded => {
if(!isExpanded) {
this.menuButton.click();
ExtendedExpectedConditions.waitForElementVisible(this.menuContainer);
}
});
}
And here 'this' is returned immediately while browser.waitForAngular() is waiting asynchronously.
initComponent() {
console.log("Initializing: " + this.isAngularComponent);
browser.waitForAngularEnabled(this.isAngularComponent);
if(this.isAngularComponent) {
console.log("Waiting for angular");
browser.waitForAngular();
}
return this;
}
you will have to refactor your functions to return promises, so you can chain them one after another.

callbacks/promises for the re-usable methods in Protractor

Should we write callbacks/promises for the re-usable methods in Page Object Pattern based testing in Protractor?
For example .. I have the below test code and Page Objects and its working fine without issues. But should I add callbacks for re-usable methods in page class?
describe('This is a test suite for Login cases',function(){
beforeEach(function() {
LoginPage.goHome();
LoginPage.doLogin();
});
afterEach(function() {
LoginPage.doLogout();
});
it('Scenario1_Login_VerifyFirstName',function(){
//Some Test step
});
Page Class:
var Login = {
PageElements: {
emailInput: element(by.css('.email')),
passwordInput: element(by.css('.password')),
loginForm: element(by.css('.form')),
logout: element(by.linkText('LOG OUT'))
},
goHome: function goHome() {
browser.get('/signin');
browser.driver.manage().window().maximize();
},
doLogin: function doLogin() {
this.PageElements.emailInput.sendKeys(UserName);
this.PageElements.passwordInput.sendKeys(Password);
this.PageElements.loginForm.submit();
},
doLogout: function doLogout() {
browser.wait(EC.elementToBeClickable(this.PageElements.profileLink));
this.PageElements.profileLink.click();
this.PageElements.logout.click();
}
};
module.exports = Login;
Yes you can.
By simply returning values or promises:
goHome: function() {
browser.get('/home');
return browser.getTitle();
},
And should resolve them on spec level inside "it" blocks like below:
it('Page should have a title', function() {
expect(Page.goHome()).toEqual('Home Page');
});

How to access Shadow Dom in Intern / Leadfoot

I'm trying to do functional tests for a Google Polymer project using InternJS.
The Web-Components part looks like the following:
<custom-element-one flex>
<custom-nested-element id="someId">
</custom-nested-element>
</custom-element-one>
The problem is that I can not access the Shadow DOM within the tests:
return this.remote
.get(require.toUrl('http://localhost:8500/test.html'))
.then(pollUntil('return document.querySelector("custom-element-one").shadowRoot;', 20000))
.findByTagName('custom-element-one')
.getProperty('shadowRoot')
.then(function (doc) {
console.log('1--------------------->>>>', doc);
console.log('2--------------------->>>>', doc.findByTagName('custom-nested-element'));
doc.findByTagName('custom-nested-element')
.getAttribute('id')
.then(function (doc) {
console.log('3--------------------->>>>', doc);
});
});
Results:
First log returns the following:
1--------------------->>>> { _elementId: '8',
_session:
{ _sessionId: 'xxxx-xxxx-xxx',
_server:
{ url: 'http://localhost:4444/wd/hub/',
sessionConstructor: [Function: ProxiedSession] },
_capabilities:
{ applicationCacheEnabled: false, ...
2--------------------->>>> { cancel: [Function], then: [Function] }
Object #<Promise> has no method 'getAttribute'
Any suggestion is appreciated.
My guess is that shadowRoot is not part of the leadFoot library yet and it is not possible to access the shadow DOM on nested
This is mostly the WebDriver issue rather than anything else. Support of Shadow DOM is very limited in WebDriver. (more).
But as a work around this, you could use pollUntil to grab the element and then get any of its attributes or call any of its exposed methods.
It would be similar to this, if you want to test value of the id attribute:
return this.remote
.get(require.toUrl('http://localhost:8500/test.html'))
.then(pollUntil(function () {
if (document.querySelector('custom-element-one').shadowRoot) {
return document.querySelector('custom-element-one').shadowRoot.querySelector('custom-nested-element').id;
}
return null;
}, [] , 20000))
.then(function (idValue) {
assert.equal(idValue, 'someId');
});

Categories

Resources