Extends ArrayStub
This class implements all the basic page object functionality, and all page objects must inherit from it. It can host selector and globalSelector fields, and will properly instantiate them as nested PageObjects when accessed. Each page object represents a DOM query that matches zero or more Elements (or subclasses of Element -- see ElementType).
PageObjects exist in a tree where each PageObject's elements
are descendants of its parent's elements. The root of the tree is a top-level
PageObject created using new PageObject()
, that by default will
match the root element (the body element or whatever was set as the root
using setRoot). It can also be constructed with a selector argument,
new PageObject(selector)
, in which case its query will be a selector query
starting from the root element. Then each non-root PageObject has a
selector and optional index that define its elements relative to its parent's
elements.
As long as no page objects have an index, each PageObject conceptually appends its selector to its parent's selector, in effect making its matching elements
rootElement.querySelectorAll(`${this.parent.selector} ${this.selector}`);
Page objects that do have an index ("indexed page objects") restrict their matching elements to only the one element at their index within their full query results (or no elements if there is no element at that index). That in effect "resets" the root element for its descendants, so its children execute queries relative to its matching element (if it has one) and so on.
PageObjects are lazy, meaning that their query is not evaluated when they are constructed, but is evaluated and re-evaluated each time a property that depends on it is accessed.
PageObjects expose an API for interacting with their matching
elements that comprises PageObject#element,
PageObject#elements, and an Array API that exposes the page
object's matching ElementTypes wrapped in indexed
PageObjects. The index operator will return an indexed
PageObject that may or may not match an element (similar to how you
can index off the end of a native array and get undefined
), while various
array iteration methods like PageObject#map generate a range of
PageObjects that reflect only the indices that actually match an
element.
Descendant PageObjects are defined by subclassing PageObject and using the selector factory function to initialize class fields.
When creating a top-level PageObject directly using new
, its query
will match the root element (the body element or whatever was )
selector
the selector to use for this page object's query (optional, default''
)parent
the element or page object to use as the root of the page object's query, defaulting to the global root element (optional, defaultnull
)index
an index to narrow the query to an element at a specific index within the query results (optional, defaultnull
)
import { PageObject, selector, setRoot } from 'fractal-page-object';
class Page extends PageObject {
list = selector('.list');
}
setRoot(rootElement);
// rootElement.querySelectorAll('.list')
new Page().list.elements;
// rootElement.querySelectorAll('.container .list')
new Page('.container').list.elements;
// rootElement.querySelectorAll('.container')[0].querySelectorAll('.list')
new Page('.container', null, 0).list.elements;
// document.body.querySelectorAll('.list');
new Page('', document.body).list.elements;
// document.body.querySelectorAll('.container .list');
new Page('.container', document.body).list.elements;
// document.body.querySelectorAll('.container')[1].querySelectorAll('.list');
new Page('.container', document.body, 1).list.elements;
This page object's single matching DOM element -- the first DOM element
matching this page object's query if this page object does not have an
index, or the index
th matching DOM element if it does have an index
specified.
Type: (ElementType | null)
This page object's list of matching DOM elements. If this page object has an index, this property will always have a length of 0 or 1.
Type: Array<ElementType>
Define a child PageObject. It can optionally be supplied with a PageObject subclass definition to allow customizing functionality on the page object, such as defining (grand)child page objects, or other helpful properties and functions. Alternatively, it can be passed an Element subclass as a type argument so elements its page objects produces will be typed more specifically.
selector
string the selector relative to the parent nodeClass
Function<PageObject>? optional PageObject subclass that can be used to extend the functionality of this page object
import { PageObject, selector } from 'fractal-page-object';
class Page extends PageObject {
list = selector('.list', class extends PageObject {
items = selector('li');
});
}
let page = new Page();
page.list.elements; // document.body.querySelectorAll('.list')
page.list.items.elements; // document.body.querySelectorAll('.list li')
import { PageObject, selector } from 'fractal-page-object';
class Page extends PageObject {
input = selector<HTMLInputElement>('input');
}
let page = new Page();
page.input.element; // type is HTMLInputElement
page.input.element.value; // no type cast needed
Returns PageObject a PageObject or PageObject subclass instance
Define a PageObject with a global scope, i.e. not scoped by its
parent page object. Useful for cases like popovers and dropdowns, where the
UI control is logically inside a given component, but all or part of it
renders elsewhere in the DOM, such as directly under the body.
globalSelector accepts a selector and optional custom class like
selector(), but the queries of the page objects it generates will be
executed from the root (document.body
or whatever was passed to
setRoot) rather than the parent page object's elements.
In some cases, it makes sense to set up a global root using setRoot, but some elements might be rendered outside even that, such as directly under the body. For cases like this, globalSelector accepts an optional root element to use as the root of the queries of the page object it generates.
args
...anyselector
string the selectorrootElement
Element optional the root element under which to query the selector.Class
Function<PageObject> optional PageObject subclass that can be used to extend the functionality of this page object
import { PageObject, selector, globalSelector } from 'fractal-page-object';
const testContainer = document.querySelector('#test-container');
setRoot(testContainer);
class Page extends PageObject {
listItems = selector('.list-item', class extends PageObject {
popover = globalSelector('.popover', class extends PageObject {
icon = selector('.icon');
});
});
}
let page = new Page();
page.listItems[0]; // testContainer.querySelectorAll('.listItems')[0]
page.listItems[0].popover; // testContainer.querySelectorAll('.popover')
page.listItems[0].popover.icon; // testContainer.querySelectorAll('.popover .icon')
import { setRoot, PageObject, selector, globalSelector } from 'fractal-page-object';
const testContainer = document.querySelector('#test-container');
setRoot(testContainer);
class Page extends PageObject {
listItems = selector('.list-item', class extends PageObject {
popover = globalSelector('.popover', document.body, class extends PageObject {
icon = selector('.icon');
});
});
}
let page = new Page();
page.listItems[0]; // testContainer.querySelectorAll('.listItems')[0]
page.listItems[0].popover; // document.body.querySelectorAll('.popover')
page.listItems[0].popover.icon; // document.body.querySelectorAll('.popover .icon')
import { PageObject, globalSelector } from 'fractal-page-object';
class Page extends PageObject {
input = globalSelector<HTMLInputElement>('input');
}
let page = new Page();
page.input.element; // type is HTMLInputElement
page.input.element.value; // no type cast needed
Returns PageObject a PageObject or PageObject subclass instance
Set the global default root element -- by default PageObjects will
only match elements that are descendants of this element. The default root
element is document.body
.
Useful for providing clarity to consumers of page-objects to provide additional context so "can't access property on undefined" errors do not public up to the consumer.
In typescript, this is also useful for type-narrowing so that you can pass on the element to other utilities.
msg
string a descriptor for what it could mean when the element doesn't existpageObject
PageObject the page object
let page = new Page();
assertExists('is the element on the page?', page);
await click(page.element);
Utility to get the fully resolved selector path of a PageObject
pageObject