Security News
ESLint is Now Language-Agnostic: Linting JSON, Markdown, and Beyond
ESLint has added JSON and Markdown linting support with new officially-supported plugins, expanding its versatility beyond JavaScript.
@lu-development/psa
Advanced tools
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!
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'))
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()
// protractor
element(by.css('input[type="password"]')).sendKeys('Str0ngP@ss')
// psa
psa.expect('input[type="password"]').toSendKeys('Str0ngP@ss')
// protractor & jasmine
expect($('button[type="submit"]').toBePresent()).toBeTruthy()
// psa
psa.expect('button[type="submit"]').toBePresent()
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)
}
})
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
psa.expect() & psa.element()
psa.expect().attribute() & psa.element().attribute()
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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.
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.
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
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.
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()
}
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.
With the PSA file in your project, psaSlowRun
and psaDebuggingStatements
will become global variables to help with writing automation.
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.
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.
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)
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.`
}
})
}
If you have any comments or questions about PSA, please do not hesitate to reach out to John Strunk.
FAQs
A JS wrapper for protractor
We found that @lu-development/psa demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 12 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
ESLint has added JSON and Markdown linting support with new officially-supported plugins, expanding its versatility beyond JavaScript.
Security News
Members Hub is conducting large-scale campaigns to artificially boost Discord server metrics, undermining community trust and platform integrity.
Security News
NIST has failed to meet its self-imposed deadline of clearing the NVD's backlog by the end of the fiscal year. Meanwhile, CVE's awaiting analysis have increased by 33% since June.