@lu-development/psa
v2.4.3
Published
A JS wrapper for protractor
Downloads
6
Maintainers
Keywords
Readme
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
- deepSearch(cssSelector)
- element(css selector | protractor element)
- expect(css selector | protractor element)
- urlContains(textToContain, sec)
psa.expect() & psa.element()
- clear(sec)
- click(sec)
- clickLink(sec)
- sendKeys(key, sec)
- isPresent(sec)
- isNotPresent(sec)
- isDisplayed(sec)
- isNotDisplayed(sec)
- isEnabled(sec)
- isNotEnabled(sec)
- getText(sec)
- containsText(textToContain, sec)
- matchText(textToMatch, sec)
- attribute(attribute)
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()
andbrower.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.