jspcom
v5.0.4
Published
TypeScript and JavaScript page component object framework for Selenium
Downloads
33
Maintainers
Readme
JsPCOM
JsPCOM is page component object framework for TypeScript and JavaScript based around Selenium.
It's built with TypeScript in mind for some fancy, more aesthetically pleasing features, but it's just as easy to use for JavaScript.
Installation
npm install jspcom
Page "Component" Object
Many have heard of the Page Object Model, but the original description wasn't particularly effective at conveying how to build something that's easy to use and maintain (and possibly with reusable parts). Many believed that page objects are god objects that have all the locators and logic for an entire page in one class, as that's what the original description unintentionally implied.
Really, though, pages are comprised of multiple nesting pieces, and not every part of the page needs to know about every other part of the page. As such, when looking to abstract interactions with a page, object composition is an ideal approach.
Each logical part of the page is effectively a component, and this extends downward to individual elements. The root component can also be an a reference to a specific element even if it has child components of its own.
This is still technically a "Page Object Framework", as it provides a Page
class for making the root Page object. But the components are where the action is at, and so that's what's emphasized.
Example
Enough about that though. Here's a quick example to help explain the concepts and get you on the move:
TypeScript
src/pages/components/login.ts
:
import { PageComponent, Component } from 'jspcom';
import { By } from 'selenium-webdriver';
class Password extends PageComponent {
locator = By.id('password');
}
class Username extends PageComponent {
locator = By.id('username');
}
export class LoginForm extends PageComponent {
locator = By.id('loginForm');
@Component()
username: Username;
@Component()
password: Password;
async fillOut(username: string, password: string) {
await this.username.sendKeys(username);
await this.password.sendKeys(password);
}
}
src/pages/login.ts
:
import { Page, Component } from 'jspcom';
import { LoginForm } from './components/login';
class LoginPage extends Page {
@Component()
loginForm: LoginForm;
async login(username: string, password: string) {
await this.loginForm.fillOut(username, password);
await this.loginForm.submit();
}
}
And then here's how you'd use it:
someScript.ts
:
// it's assumed that driver is a WebDriver instance
const page = new LoginPage(driver);
await page.login('myusername', 'mypassword');
JavaScript
src/pages/components/login.js
:
const { By } = require('selenium-webdriver');
const { PageComponent } = require('jspcom');
class Password extends PageComponent {
locator = By.id('password');
}
class Username extends PageComponent {
locator = By.id('username');
}
export class LoginForm extends PageComponent {
locator = By.id('loginForm');
componentMappings = {
username: Username,
password: Password
}
async fillOut(username: string, password: string) {
await this.username.sendKeys(username);
await this.password.sendKeys(password);
}
}
src/pages/login.js
:
const { Page } = require('jspcom');
const { LoginForm } = require('./components/login');
class LoginPage extends Page {
componentMappings = {
loginForm: LoginForm
}
async login(username: string, password: string) {
await this.loginForm.fillOut(username, password);
await this.loginForm.submit();
}
}
And then here's how you'd use it:
someScript.js
:
// it's assumed that driver is a WebDriver instance
const page = new LoginPage(driver);
// always necessary to parse components
await page.loaded;
await page.login('myusername', 'mypassword');
Waiting for things to load
Both Page
and PageComponent
objects will automatically try to kick off all of the "conditions" each one has as they are instantiated. "Conditions" are plain callables that return either true
or false
. These callables are called repeatedly until the timeout is reached, or until all of them return true
. If the timeout is reached, an error will be thrown, and if the callables throw an error, that error will bubble up, so make sure to handle errors appropriately. The timeout
and pollRate
are properties of the pages and components that can be set to adjust how long and how often it will try to check.
The components are only instantiated when you reference them though, so it's recommended to do this either in each component's manager's conditions
or by overriding the component's manager's wait
method.
:warning: Always await .loaded
, and never call .wait
directly
Due to how JavaScript works, it's not currently possible to make sure the component mapping is attached to the instance object before the framework attempts to parse that mapping in the constructor alone (as super calls will be necessary and it could get pretty boilerplate-y in the constructors themselves). As a result, it's always necessary to call await page.loaded
before trying to do anything with the page object (so ideally, this would be done right after instantiating it).
Related to this, is also necessary to never call .wait
directly. The reason for this, is because .loaded
is a getter that triggers the component parsing right before it makes a call to .wait
. .loaded
returns the Promise
provided by .wait
, so that's what JS would be awaiting, but it needs to have the components parsed before then.
Waiting logic example
Let's say you want the LoginPage
to wait for the page to be fully rendered before proceeding, as the LoginForm
is rendered after the document.readystate
has been set to complete
because it was rendered via JavaScript, but you know that as long as the form is actually rendered, you should be good. Here's how that could look:
TypeScript
src/pages/components/login.ts
:
import { PageComponent, Component } from 'jspcom';
import { By } from 'selenium-webdriver';
class Password extends PageComponent {
locator: Locator = By.id('password');
}
class Username extends PageComponent {
locator: Locator = By.id('username');
}
export class LoginForm extends PageComponent {
locator: Locator = By.id('loginForm');
@Component()
username: Username;
@Component()
password: Password;
conditions: <() => any>[] = [
() => this.isDisplayed(),
]
async fillOut(username: string, password: string) {
await this.username.sendKeys(username);
await this.password.sendKeys(password);
}
}
src/pages/login.ts
:
import { Page, Component } from 'jspcom';
import { LoginForm } from './components/login';
class LoginPage extends Page {
@Component()
loginForm: LoginForm;
async wait() {
// never call `wait` directly
await this.loginForm.loaded;
}
async login(username: string, password: string) {
await this.loginForm.fillOut(username, password);
await this.loginForm.submit();
}
}
And then here's how you'd use it:
someScript.ts
:
// it's assumed that driver is a WebDriver instance
const page = new LoginPage(driver);
await page.loaded;
await page.login('myusername', 'mypassword');
JavaScript
The only thing that changes between TypeScript and JavaScript (other than the @Component()
stuff), is that JavaScript wouldn't have the type annotations.
Staleness check (and other non-initialization waits)
Checking for staleness is tricky, because it relies on having a reference to the WebElement
beforehand. This framework doesn't fetch WebElement
references until it absolutely has to, and even then, it doesn't hold onto them. But, it does have a special private attribute (stalenessCache
) and two special methods (cacheElementForStalenessCheck
and cacheHasGoneStale
) that are used specifically for enabling this sort of check.
Here's how it can be used:
class SignInForm extends PageComponent {
locator = By.css('#signIn');
@Component()
username: Username;
@Component()
password: Password;
@Component()
signInButton: SignInButton;
@Component()
errorMessage: ErrorMessage;
get conditions() {
return [
() => this.isDisplayed(),
];
}
async fillOut(username, password) {
await this.username.sendKeys(username);
await this.password.sendKeys(password);
}
async submit() {
await this.cacheElementForStalenessCheck();
await this.signInButton.click();
await this.driver.wait(() => {
if (await this.cacheHasGoneStale()) {
return true;
}
if (await this.errorMessage.isPresent()) {
throw new SignInError(await this.errorMessage.getText());
}
return false;
});
}
}
In this example, the submit method for the SignInForm
knows it has to wait for either the form's WebElement
to be removed from the DOM, or for the error message to show up after the sign in button has been clicked.
If the element is removed from the DOM (which it can tell if the reference went stale), then it knows the sign in succeeded, and it can return true
.
If the element is still there, but no error has shown up, then it can return false
, indicating it should attempt to check again.
Throwing an error
This example uses the driver.wait
method provided by selenium-webdriver
. This method accepts either a special Condition
object, which is made from a Condition
class also provided by selenium-webdriver
, or a simple callable that return something true
or false
whenever it's called.
If the form's element is still in the DOM, but an error message has shown up, this will throw an error, which will break out of the wait
call, and bubble the error up.
An error is thrown in the example callable to indicate that something happened that deviated from the "normal" flow. Even if the intent was to make that error show up, the page object doesn't need to know that your intent at some point would be exceptional.
This is extremely helpful in the context of automated checks (and code in general), because it makes it easier to convey intent in the code. In this case, if you wanted to make sure that bad credentials made the error message show up with particular text, you could have the error have that text as its message, and then you can assert that an error with that message was thrown.
Even better, when you intended it to get through signing in without issues, but one occurred, that error could contain some extremely helpful information to indicate what went wrong before investing more time into debugging it.
Working with iFrames
iFrames are tricky, as you need to switch to them before you can do anything inside of them, or even to see inside of them at all. Some convenience methods have been provided for you to use and build off of when working with iFrames. Here's a quick example:
class Something extends PageComponent {
locator = By.css('.thing');
async click() {
await this.parent.switchTo();
await this.click();
await this.switchToParentFrame();
}
}
class SomeIframe extends IframePageComponent {
locator = By.css('iframe');
@Component
something: Something;
conditions = [
this.iFrameIsReady,
];
async doTheThing() {
await this.something.click();
}
}