@lu-development/psa

2.4.3 • Public • Published

Using the Protractor Smart Actions wrapper module:

The PSA module is a wrapper for the protractor functions which make them more flexible and dependable. Using it, you should no longer need to use browser.sleep(), browser.wait(), or worry about shadow doms in your tests. PSA is not a replacement for Protractor, PSA is an extension to use alongside Protractor. The current version of PSA is 2.4.2

Now with version 2.4.2 and above, elements within iframes are also visible by default!

Importing PSA into your specs

PSA can be added to your repo by installing the package through npm:

// install with npm
npm install @lu-development/psa

PSA can be used in any spec by and adding the following require to any file:

// import into your spec file
const psa = new(require('@lu-development/psa'))

Using PSA in your specs

PSA's general syntax is psa.expect(ELEMENT).ACTION() or psa.element(ELEMENT).ACTION():

// a protractor element can passed as the ELEMENT
let el = element(by.css('#submitBtn'))
psa.element(el).click()
// or a css selector can be passed as the ELEMENT
psa.element('#submitBtn').click()
PSA vs Protractor syntax:
// protractor
element(by.css('input[type="password"]')).sendKeys('Str0ngP@ss')
// psa
psa.expect('input[type="password"]').toSendKeys('Str0ngP@ss')
PSA vs Protractor & Jasmine syntax:
// protractor & jasmine
expect($('button[type="submit"]').toBePresent()).toBeTruthy()
// psa
psa.expect('button[type="submit"]').toBePresent()
Test Spec Using Regular Protractor Functions and a Page Object:
it('Enter information for US or international students', async () => {
	if (user.country == "United States") {
		await claimFormPage.SSN().sendKeys(user.ssn)
		await browser.sleep(500)
		await claimFormPage.zip().sendKeys(user.zip)
		await browser.sleep(500)
	} else {
		await claimFormPage.intCheckBox().click()
		await browser.sleep(500)
		expect(await claimFormPage.intBanner().toBeDisplayed()).toBeTruthy()
		await claimFormPage.country().sendKeys(user.country)
		await browser.sleep(500)
		let phoneNum = user.phones.length > 1 ? user.phones[1] : user.phones[0]
		await claimFormPage.phone().sendKeys(phoneNum)
		await browser.sleep(500)
	}
})
Test Spec Using PSA and a Page Object:
it('Enter information for US or international students', async () => {
	if (user.country == "United States") {
		await psa.expect(claimFormPage.SSN).toSendKeys(user.ssn)
		await psa.expect(claimFormPage.zip).toSendKeys(user.zip)
	} else {
		await psa.expect(claimFormPage.intCheckBox).toClick()
		await psa.expect(claimFormPage.intBanner).toBeDisplayed()
		await psa.expect(claimFormPage.country).toSendKeys(user.country)
		let phoneNum = user.phones.length > 1 ? user.phones[1] : user.phones[0]
		await psa.expect(claimFormPage.phone).toSendKeys(phoneNum)
	}
})

PSA Function Definitions

psa

psa.expect() & psa.element()

psa.expect().attribute() & psa.element().attribute()

deepSearch

Locates an element using a CSS selector. If the element is not present in the DOM, first level shadow DOMs and iframes are searched for the element.
Usage: psa.deepSearch('button[type="submit"]')

Arguments:

arg type description
cssSelector string The CSS selector to use

Returns:
A promise which resolves to a protractor element
 

element

Returns a psa element using the protractor element or css selector passed. All functions called from the psa element returned will return false if they do not resolve. If a css selector is passed as the element, PSA will search all first level shadow DOMs and iframes for the element if it is not present in the DOM.
Usage: psa.element('button[type="submit"]')
Alias: psa.e('button[type="submit"]')

Arguments:

arg type description
element string or protractor element The CSS selector or protractor element to use

Returns:
A promise which resolves to a psa element
 

expect

Returns a psa element using the protractor element or css selector passed. All functions called from the psa element returned will throw an error if they do not resolve, failing any jasmine specs. If a css selector is passed as the element, PSA will search all first level shadow DOMs and iframes for the element if it is not present in the DOM.
Usage: psa.expect('button[type="submit"]')

Arguments:

arg type description
element string or protractor element The CSS selector or protractor element to use

Returns:
A promise which resolves to a psa element
 

urlContains

Checks if the current page url contains the passed text until the passed amount of seconds pass or the url contains the text passed Usage: psa.urlContains('www.liberty.edu')

Arguments:

arg type description
textToContain string The text to check the URL for
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

clear

Waits until an element is present, displayed, and enabled, then tries to clear the element's input field for the passed amount of seconds or until the element's input field is able to be cleared.
Usage: psa.expect('input[type="userName"]').clear()
Alias: psa.expect('input[type="userName"]').toClear()

Arguments:

arg type description
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

click

Waits until an element is present, displayed, and enabled, then tries to click on the element for the passed amount of seconds or until the element is able to be clicked.
Usage: psa.expect('button[type="submit"]').click()
Alias: psa.expect('button[type="submit"]').toClick()

Arguments:

arg type description
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

clickLink

Waits until an element is present, displayed, and enabled, then tries to click on the element for the passed amount of seconds or until the element is no longer present in the DOM. This method is faster and more reliable than the click function and should be used when applicable.
Usage: psa.expect('button[type="submit"]').clickLink()
Alias: psa.expect('button[type="submit"]').toClickLink()

Arguments:

arg type description
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

sendKeys

Waits until an element is present, displayed, and enabled, then clears the element's input field and tries to send keys to the element for the passed amount of seconds or until the element is able to be sent keys.
Usage: psa.expect('input[type="user"]').sendKeys('jrstrunk')
Alias: psa.expect('input[type="user"]').toSendKeys('jrstrunk')

Arguments:

arg type description
keys string The value to be sent to the element
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

isPresent

Checks if an element is present in the DOM for the passed amount of seconds or until the element is present in the DOM. When checking if an element is present on the current page to be further interacted with, isDisplayed should be used over isPresent since isDisplayed calls isPresent as a prerequisite.
Usage: psa.expect('button[type="submit"]').isPresent()
Alias: psa.expect('button[type="submit"]').toBePresent()

Arguments:

arg type description
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

isNotPresent

Checks if an element is not present in the DOM for the passed amount of seconds or until the element is not present in the DOM.
Usage: psa.expect('button[type="submit"]').isNotPresent()
Alias: psa.expect('button[type="submit"]').toBeNotPresent()

Arguments:

arg type description
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

isDisplayed

Checks if an element is present and displayed in the DOM for the passed amount of seconds or until the element is displayed in the DOM. Will try to scroll to the element if it is not found to be displayed.
Usage: psa.expect('button[type="submit"]').isDisplayed()
Alias: psa.expect('button[type="submit"]').toBeDisplayed()

Arguments:

arg type description
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

isNotDisplayed

Checks if an element is present and not displayed or not present at all in the DOM for the passed amount of seconds or until the element is not displayed.
Usage: psa.expect('button[type="submit"]').isNotDisplayed()
Alias: psa.expect('button[type="submit"]').toBeNotDisplayed()

Arguments:

arg type description
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

isEnabled

Checks if an element is present and enabled for the passed amount of seconds or until the element is enabled.
Usage: psa.expect('button[type="submit"]').isEnabled()
Alias: psa.expect('button[type="submit"]').toBeEnabled()

Arguments:

arg type description
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

isNotEnabled

Checks if an element is present and not enabled or not present at all in the DOM for the passed amount of seconds or until the element is not enabled.
Usage: psa.expect('button[type="submit"]').isNotEnabled()
Alias: psa.expect('button[type="submit"]').toBeNotEnabled()

Arguments:

arg type description
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

getText

Waits until the element is present and displayed, then tries to get the element's text for the passed amount of seconds or until the element's text has a non-empty value. Note: protractor's getText function will return '' if the element has no text, but this function will throw an error or return false if the element has no text.
Usage: let bannerText = psa.expect('p[id="mainBanner"]').getText()
Alias: let bannerText = psa.expect('p[id="mainBanner"]').toGetText()

Arguments:

arg type description
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to the passed element's text or false
 

containsText

Checks if an element's text contains the passed text for the passed amount of seconds or until the element's text contains the passed text.
Usage: psa.expect('p[id="mainBanner"]').containsText('Welcome to my website!')
Alias: psa.expect('p[id="mainBanner"]').toContainText('Welcome to my website!')

Arguments:

arg type description
textToContain string The text to check the element for
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

matchText

Checks if an element's text matches the passed text for the passed amount of seconds or until the element's text matches the passed text.
Usage: psa.expect('p[id="mainBanner"]').matchText('Welcome to my website!')
Alias: psa.expect('p[id="mainBanner"]').toMatchText('Welcome to my website!')

Arguments:

arg type description
textToMatch string The text to check the element for
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

attribute

Returns a psa attribute object of the attribute name passed.
Usage: psa.expect('p[id="mainBanner"]').attribute('disabled')

Arguments:

arg type description
attribute string The name of the element's attribute to inspect
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to a psa attribute object
 

attribute.matches

Checks if an element's attribute value matches the passed value for the passed amount of seconds or until the element's attribute value matches the passed value.
Usage: psa.expect('p[id="mainBanner"]').attribute('disabled').matches('yes-it-is')
Alias: psa.expect('p[id="mainBanner"]').attribute('disabled').toMatch('yes-it-is')

Arguments:

arg type description
value string The value to check the element attribute for
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

attribute.contains

Checks if an element's attribute value contains the passed value for the passed amount of seconds or until the element's attribute value contains the passed value.
Usage: psa.expect('p[id="mainBanner"]').attribute('disabled').contains('yes')
Alias: psa.expect('p[id="mainBanner"]').attribute('disabled').toContain('yes')

Arguments:

arg type description
valueToContain string The value to check the element attribute for
sec int or float The number of seconds to wait before timing out. The default value is 5

Returns:
A promise which resolves to true or false
 

PSA 2.3.0 Is here!

Whats new?
There is now a PSA clear and getText function! Also the isDisplayed function will automatically scroll to elements that need to be scrolled to before they can be clicked, sent keys to, etc.

The PSA Rational

PSA was developed because of the problems and flakey-ness caused by static sleeps in automation, protractor's expected conditions not always performing correctly, and automating elements in the shadow dom & iframes just being a big hassle.

PSA solves these issues by retrying each action until it is completed or times out and by doing an automatic search of shadow doms and iframes when it is called. This means that browser.sleep() and browser.wait() do not need to be used at any point in tests and shadow dom & iframe elements do not need any more complex selectors! These are two fewer things the tester needs to worry about accounting for while writing automation.

The Pros of Using PSA:

  • Cleaner Test Code: browser.sleep() and brower.wait() statements do not take up any lines of code.

  • Quicker Test Execution Time: Only the time needed to perform the action down to the tenth of a second is taken. Generally when sleeps are called, they waste some amount of time.

  • More Robust Test Executions: If a page takes 2 seconds longer to load this run, the test won't fail. Since actions are retried, the tests work with inconsistent browser loading times.

  • Better Control Flow Statements: The element function can be used for complex control flow, as discussed below.

  • Easier to Write Tests: The programmer does not need to worry about the timing of their browser actions.

  • Less Time Needed to Write Tests: Since the programmer does not need to spend time thinking about the timing of browser actions, the total time it takes to automate a test is reduced.

  • Shorter Syntax for Jasmine expect statements: PSA's expect function behaves exactly like Jasmine's expect function, but has a more concise syntax

The Cons of Using PSA:

  • A new tool to learn: It is fairly simple, but the team would have to spend time learning what it does.

  • Longer Syntax for Each Browser Action: psa.expect() has to be added on the front of every line that calls a browser action.

More Details On How PSA works:

Under the hood, PSA simply works like this: Note: this is not actually the PSA module code, but is logically accurate to what it does

// tries to click an element for 5 seconds
psa.click = async () => {
	for (let var i = 0; i < 50; i++) {
		try {
			await this.protractorElement.click()
			break
		} catch {
			await browser.sleep(100)
		}
	}
	if (i == 50) {
		throw 'Error: psa.click() failed - could not find element within 5 seconds'
	} else {
		return true
	}
}

It tries to click on an element every tenth of a second for 5 seconds. Once it clicks on the element, it will break out of the loop and return true. If it is not able to click on the element for 5 seconds, it will throw an error.

Each PSA function has a sec argument. The sec argument should be a number (whole or decimal) and it determines how many seconds it takes for the action to time out.

The difference between psa.element and psa.expect illustrated:
This code checks if there is an error message displayed on the page, and if there is, it enters the password again:

// if signing in failed, enter the password again and continue
if(await psa.element('errorMsg[msgType="password"]').isDisplayed(sec=1)) {
	await $('input[type="password"]').clear()
	await psa.expect('input[type="password"]').sendKeys('password')
	await psa.expect('button[type="submit"]').click()
}

When logging into Azure with a mylu guest account, often times a prompt would appear asking if the user wanted to stay logged in, which would break the automation. Since it didn't appear every time, it could not be added to the automation like everything else. This was easy to handle using PSA:

// if ms asks prompts the user to stay signed in, click no and continue
if(await psa.element(msSignIn.doNotStaySignedInButton()).isPresent(1)) {
	await psa.expect(msSignIn.doNotStaySignedInButton()).click()
}

Control Flow Statement Notes:

Using PSA functions for control flow can be tricky at times, so make sure you consider these four unique situations as you are writing specs. Note: psa.e() is shorthand for psa.element()

// Good when expecting an error msg to appear
// Logic: As soon as an error msg appears within one second, log 'Error is displayed'
if (psa.e('#errorMsg').isDisplayed(1)) { 
	console.log('Error is displayed') 
}

// Good when expecting an error msg to disappear
// Logic: As as soon as an error msg is not displayed within one second, log 'Error is not displayed'
if (psa.e('#errorMsg').isNotDisplayed(1)) { 
	console.log('Error is not displayed') 
}

// Good when expecting an error msg to not appear
// Logic: If an error msg does not appear within one second, log 'Error is not displayed'
if (!(psa.e('#errorMsg').isDisplayed(1))) {
	console.log('Error is not displayed')
}

// Good when expecting an error msg to not disappear
// Logic: If an error msg does not disappear within one second, log 'Error is displayed'
if (!(psa.e('#errorMsg').isNotDisplayed(1))) {
	console.log('Error is displayed')
}

These four control flow statements apply to all psa functions that check if an element is something.

Other PSA Features

With the PSA file in your project, psaSlowRun and psaDebuggingStatements will become global variables to help with writing automation.

psaSlowRun

You can set this variable to true or false either at the top of the utils.psa.js file or anywhere in your it blocks. Sometimes, PSA functions click and inspect elements too fast for a user to follow on screen. If psaSlowRun is set to true, a half a second pause will be put in between each action, so it gives the user enough time to watch what the automation is actually doing and can verify it is doing the correct things. This is most useful if you target a specific set of commands in an it block and set it equal to true before those commands, and false after those commands. Here is an example if it's usage:

describe('Navigate to the validate user form and submit correct user data', () => {
	it('The User Validation Page should load', async () => {
		psaSlowRun = true
		await browser.get(this.validateUserLink)
		await psa.expect(vp.logo).toBeDisplayed()
	})

	validateSnippet.enterInfo(data.missingRiskVlidateUser)
	validateSnippet.submitForm()
	validateSnippet.checkCodePage()
	
	it('Save the user validation code', async () => {
		this.userValidationCode = await $(vp.codePageCode).getText()
		psaSlowRun = false	
	})
})

Only the code that is run between when psaSlowRun was set to true and when it was set back to false will run slowly, allowing the tester to more easily follow what is happening on screen.

psaDebuggingStatements

You can set this variable to true or false either at the top of the utils.psa.js file or anywhere in your it blocks. Sometimes, it's hard to tell exactly what function call is doing. If psaDebuggingStatements is set to true, PSA will log every function call, value check of an element / attempt to perform an action on it, and every resolution of a function call. This is most useful if you target a specific set of commands in an it block and set it equal to true before those commands, and false after those commands.

describe('Navigate to the validate user form and submit correct user data', () => {
	it('The User Validation Page should load', async () => {
		await  browser.get(this.validateUserLink)
		psaDebuggingStatements = true
		await  psa.expect(vp.logo).toBeDisplayed()
	})

	validateSnippet.enterInfo(data.missingRiskVlidateUser)
	validateSnippet.submitForm()
	validateSnippet.checkCodePage()
	
	it('Save the user validation code', async () => {
		this.userValidationCode = await $(vp.codePageCode).getText()
	})
})

This will output very verbose messages to the console about what is happening every tenth of a second as PSA checks the vp.logo element's display value.

Performing an action only n times

The sec argument is not actually the number of seconds that psa will retry something, sec is the number of iterations / 10 that psa will try something, with a tenth of a second pause in between iterations. So sec = 5 is 50 iterations with a tenth of a second of a pause in between, which ends up taking roughly 5 seconds. Knowing this, you can use the sec argument better, for instance if you want to check if something is present only ONCE and not again, the following logic can be used:

// check if an element is present exactly once, since total iterations = 0.1 * 10 = 1
await psa.expect(page.header).isPresent(0.1)

// check if an element is present exactly twice, since total iterations = 0.2 * 10 = 2
await psa.expect(page.header).isPresent(0.2)

Adding to PSA

I highly encourage anyone using PSA to read through the source code to get a good understanding of how exactly PSA works under the hood. If you see any room for improvement for PSA, please create a new branch then create a Pull Request for your changes! They will be greatly appreciated.
PSA was designed in a specific way to allow for easy additions to it! Protractor has many many functions which are not all wrapped by PSA, so if you need to use a protractor function that is not already wrapped by a PSA function, then please add to the project! The template for wrapping PSA around any protractor element function is like so (placeholders that should be changed are shown in all caps):

FUNCNAME = async (FUNCARG, sec = 5) => {
	psaReporter(`FUNCNAME() call: Checking if element ${this.protractorElementSelector} DESC OF FUNC PURPOSE`)
	ANY PREREQUISITE CONDITIONS
	return await retryLoop(sec, FUNCARG, async (sec, expectedValue) => {
		let result = await this.protractorElement.PROTRACTORFUNC()
		return {
			'value': result,
			'boolValue': BOOLEAN EXPRESSION REPRESENTING THE PROTRACTOR FUNCTION CALL PASS CRITEREA == expectedValue,
			'progressMsg': `MESSAGE TO DISPLAY AS PSA TRIES THE PROTRACTOR FUNCTION CALL`,
			'trueMsg': `Element ${this.protractorElementSelector} DESC OF WHY IT PASSED`,
			'falseMsg': `Element ${this.protractorElementSelector} DESC OF WHY IT FAILED`,
			'errorMsg': `psa.matchText() failed - element ${this.protractorElementSelector} DESC OF WHY IT FAILED within ${sec} seconds.`
		}
	})
}

Here is an example of what a psa function looks like when it is complete. Note that the third argument in the retryLoop function call is passed into the expectedValue argument of the arrow function being passed as the fourth argument to the retryLoop call:

// checks if an element's text matches the textToMatch within sec seconds
matchText = async (textToMatch, sec = 5) => {
	psaReporter(`matchText() call: Checking if element ${this.protractorElementSelector} text matches ${textToMatch}`)
	// make sure the element is present before getting the text
	await this.isPresent(sec)
	return await retryLoop(sec, textToMatch, async (sec, expectedValue) => {
		let  getTextResult = await  this.protractorElement.getText()
		return {
			'value': getTextResult,
			'boolValue': getTextResult == expectedValue,
			'progressMsg': `Comparing '${getTextResult}' to '${expectedValue}'`,
			'trueMsg': `Element ${this.protractorElementSelector} text matches '${expectedValue}'!`,
			'falseMsg': `Element ${this.protractorElementSelector} text does not match '${expectedValue}'!`,
			'errorMsg': `psa.matchText() failed - element ${this.protractorElementSelector} text '${getTextResult}' did not match '${expectedValue}' within ${sec} seconds.`
		}
	})
}

Here is an example of a psa function that does not need to take in any arguments:

// checks if the element is present within sec seconds
isPresent = async (sec = 5) => {
	psaReporter(`isPresent() call: Checking if element ${this.protractorElementSelector} is present`)
	return await retryLoop(sec, true, async (sec, expectedValue) => {
		let isPresentResult = await this.protractorElement.isPresent()
		return {
			'value': isPresentResult,
			'boolValue': isPresentResult == expectedValue,
			'progressMsg': `Checking if element ${this.protractorElementSelector} is present`,
			'trueMsg': `Element ${this.protractorElementSelector} is present!`,
			'falseMsg': `Element ${this.protractorElementSelector} is not present!`,
			'errorMsg': `psa.isPresent() failed - could not find element ${this.protractorElementSelector} within ${sec} seconds.`
		}
	})
}

And finally here is an example of a psa function that takes two arguments:

// checks if an element's attribute equals value within sec seconds
matchAttribute = async (attribute, value, sec = 5) => {
	psaReporter(`matchAttribute() call: Checking if element ${this.protractorElementSelector}  ${attribute} attribute equals ${value}`)
	// make sure the element is present before getting it's text
	await this.isPresent(sec)
	return await retryLoop(sec, {'attribute':attribute, 'value': value}, async (sec, expectedValue) => {
		let getAttributeResult = await  this.protractorElement.getAttribute(expectedValue['attribute'])
		return {
			'value': getAttributeResult,
			'boolValue': getAttributeResult == expectedValue['value'],
			'progressMsg': `Comparing '${getAttributeResult}' to equal '${expectedValue['value']}'`,
			'trueMsg': `Element ${this.protractorElementSelector} attribute equals '${expectedValue['value']}'!`,
			'falseMsg': `Element ${this.protractorElementSelector} attribute does not equals '${expectedValue['value']}'!`,
			'errorMsg': `psa.matchAtteibute() failed - element ${this.protractorElementSelector} attribute '${getAttributeResult}' did not equal '${expectedValue['value']}' within ${sec} seconds.`
		}
	})
}

Thanks for reading this far :)

If you have any comments or questions about PSA, please do not hesitate to reach out to John Strunk.

Readme

Keywords

Package Sidebar

Install

npm i @lu-development/psa

Weekly Downloads

1

Version

2.4.3

License

ISC

Unpacked Size

85.5 kB

Total Files

7

Last publish

Collaborators

  • brmoore
  • ericjameswilliamson
  • mdconnors
  • thelmsdevguy
  • rjpfeiffer1
  • tnprescott
  • egborrero
  • drcrankshawlu