Map
This guide provides a detailed explanation of wdio-workflo's PageElementMap
and
ValuePageElementMap
classes. However, it does not show you how to customize these
classes. If you want to learn how to create your own, customized map classes
by extending wdio-workflo's PageElementMap
or ValuePageElementMap
class, read the
Customizing a Map guide.
Overview and Objective
Wdio-workflo's PageElementMap
manages a static collection of PageElement
instances
of the same class. Static means that the contents of the collection/the individual page elements managed by the collection are already known at compile time and do not change during runtime.
Some typical website components that should be mapped by a PageElementMap
are a the links in a navigation menu, because they are usually known at the time
of test creation and do not change during the runtime of your web application.
All PageElement
instances managed by a PageElementMap
share a common
base XPath selector, for example, they could all be HTML link <a>
elements
located by the XPath selector //a[@role="navigationLink"]
. Although the
page elements all share the same base XPath selector, each of them can be
accessed via a unique key. In order for this to work, all page elements of
a map need to have unique values for a certain HTML attribute.
The PageElementMap
class maps its managed PageElement
instances to the
aforementioned unique keys based on the HTML attribute values you provided.
If this sounds very theoretical to you, there are plenty of practical examples
and explanations further down this guide. All in all, the objective of the
PageElementMap
class is to greatly reduce the amount of code needed to define
a couple of similar PageElement
instances.
Finally, like the PageElement
and the PageElementList
classes, PageElementMap
also features a currently
and an eventually
API to check if some, none or all
of the map's elements currently/eventually have a certain state, and a wait
API
to wait for some, none or all of the map's elements to reach a certain state.
PageElementMap
Creating a ElementMap()
Factory Method
Instead of manually invoking the constructor of PageElementMap
using the new
keyword,
you should always call the ElementMap()
factory method of the PageNodeStore
class to create an instance of the PageElementMap
class:
import { stores } from '?/page_objects';
const linkMap = stores.pageNode.ElementMap('//a', {
identifier: {
mappingObject: {
demo: 'Demo Page',
examples: 'Examples',
api: 'API'
},
mappingFunc: (baseSelector, value) => xpath(baseSelector).text(value)
}
});
As you can see from the example above, the ElementMap()
factory method requires
us to pass an identifier
property in its publicly configurable opts
parameter.
To understand how the identifier
property works, let us examine the type parameters
and constructor of PageElementMap
in more detail!
Type Parameters
The PageElementMap
class has four type parameters:
- The type of the
PageNodeStore
associated with the map to create other page nodes. - The unique keys used to access the page elements managed by the map.
- The class type of the page elements managed by the map.
- The type of the
opts
parameter of a single page element managed by the map.
export class PageElementMap<
Store extends PageNodeStore,
K extends string,
PageElementType extends PageElement<Store>,
PageElementOpts extends Partial<IPageElementOpts<Store>>
> extends PageNode<Store>
Constructor
The constructor of PageElementMap
requires two parameters:
- The XPath selector that locates all of the map's page elements on a website.
- The
opts
parameter containing properties to configure thePageElementMap
constructor(
selector: string,
opts: IPageElementMapOpts<Store, K, PageElementType, PageElementOpts>,
) { /*...*/ }
The most important properties of the opts
parameter are:
store
=> ThePageNodeStore
instance associated with thePageElementMap
.identifier
=> An object containing all information required to map the page elements of a map to a set of unique keys.timeout
=> The map's default timeout for all functions of theeventually
andwait
APIs.interval
=> The map's default interval for all functions of theeventually
andwait
APIs.elementStoreFunc
=> The factory method used to create a single map element.elementOpts
=> Theopts
parameter passed toelementStoreFunc
to create a map element.
identifier
Object
The identifier
object, which is passed to the constructor of a PageElementMap
as a property of the opts
parameters object, contains all information required
to map the PageElement
instances managed by a PageElementMap
to a set of
unique keys. It consists of two properties: mappingObject
and mappingFunc
.
As already mentioned, the precondition for using a PageElementMap
is that all
of its PageElement
instances have a unique value for a certain HTML attribute.
Simply speaking, the task of the mappingFunc
is to define which kind of
HTML attribute is to be used for the mapping process, whereas the mappingObject
defines the unique values of this HTML attribute for each PageElement
instance
and the unique keys used to access these PageElement
instances on the map.
During the mapping process, PageElementMap
iterates over all key-value pairs
of the mappingObject
and invokes mappingFunc
for each pair, passing it
the base XPath selector that identifies all page elements of a map as first parameter
and the unique HTML attribute value of the currently iterated entry of mappingObject
as second parameter. mappingFunc
now uses the base XPath selector and the unique
HTML attribute value to create an XPath expression that uniquely identifies
each of the map's PageElement
instances. Finally, PageElementMap
creates
a PageElement
instance for each unique XPath expression and assigns the
created page element to the corresponding key of the mappingObject
.
To help you better understand this mapping process, let's revisit our
ElementMap()
factory method code example from above:
import { stores } from '?/page_objects';
const linkMap = stores.pageNode.ElementMap('//a', {
identifier: {
mappingObject: {
demo: 'Demo Page',
examples: 'Examples',
api: 'API'
},
mappingFunc: (baseSelector, value) => xpath(baseSelector).text(value)
}
});
In this example, our PageElementMap
manages three PageElement
instances,
each wrapping an HTML link <a>
element - hence the base XPath selector is '//a'
.
Looking a the mappingFunc
implementation, we can see that the HTML attribute
used to uniquely identify each of these PageElement
instances is the text
of
the HTML element. Therefore, the values of our mappingObject
represent the texts
of the HTML link elements, and its keys determine the names by which we will later be
able to access each PageElement
instance of the map.
The following section of this guide shows you how we can access the PageElement
instances managed by a PageElementMap
via the keys defined in the mappingObject
.
Accessing Map Elements
In the previous section of this guide, I explained the mapping process of
PageElementMap
that links each of the map's PageElement
instances to a
unique key.
To access these PageElement
instances, we need to use the $
accessor of the
PageElementMap
class. The $
accessor returns an object whose keys are taken
from mappingObject
and whose values are the PageElement
instances assigned
to the respective key during the mapping process:
So if we wanted to click on the api
link of our linkMap
from the above
code example, we would write:
linkMap.$.api.click();
State Functions
State Retrieval Functions
State retrieval functions of the PageElementMap
class fulfil the same purpose
as those of the PageElement
class: For each PageElement
instance managed by the
map, they retrieve the value of a certain attribute of the HTML element that is
wrapped by PageElement
from the website. They return an object whose keys are
taken from the map's mappingObject
and whose values represent the values
of the retrieved HTML attribute for each PageElement
:
import { stores } from '?/page_objects';
const linkMap = stores.pageNode.ElementMap('//a', {
identifier: {
mappingObject: {
demo: 'Demo Page',
api: 'API'
},
mappingFunc: (baseSelector, value) => xpath(baseSelector).text(value)
}
});
const linkTexts = linkMap.getText();
In the above examples, the linkTexts
variable would now store
{demo: 'Demo Page', api: 'API'}
;
Internally, PageElementMap
simply iterates over all of its managed
PageElement
instances and invokes the respective state retrieval function
on each PageElement
. You can also skip the invocation of the state retrieval
function for certain PageElement
instances by using a filter mask. The
Filter Masks section of this guide shows you how to do that.
For more information about the types of available state retrieval functions,
please read the State Retrieval Functions section
of the PageElement
guide. Please note that not all types of PageElement
state
retrieval functions are also available on a PageElementMap
.
Action Functions
Action functions change the state of the tested web application by interacting
with HTML elements that are mapped by PageElement
instances. To execute an
action function on each PageElement
instance managed by a PageElementMap
,
you have two options:
- You can access each
PageElement
instance via the$
accessor and invoke an action function on each. - You can use the
eachDo()
method of thePageElementMap
which automatically loops over the managedPageElement
instances and invokes an action function which you need to pass toeachDo()
on each page element. UsingeachDo()
allows you to optionally pass a filter mask as second parameter to skip the action function's invocation for certainPageElement
instances.
The following code example compares both options for executing action functions on each element of a map:
import { stores } from '?/page_objects';
const linkMap = stores.pageNode.ElementMap('//a', {
identifier: {
mappingObject: {
demo: 'demoLink',
examples: 'examplesLink',
api: 'apiLink'
},
mappingFunc: (baseSelector, value) => xpath(baseSelector).id(value)
},
elementOpts: { waitType: Workflo.WaitType.text }
});
// Click on each page element of the map after accessing the page elements via the `$` accessor.
linkMap.$.demo.click();
linkMap.$.examples.click();
linkMap.$.api.click();
// Clicks on the `demo` and the `api` but not on the `examples` page element of
// `linkMap` because `examples` is not included in the filter mask.
linkMap.eachDo(
linkElement => linkElement.click(),
{
demo: true,
api: true
}
);
For more information about the types of available action functions,
please read the Action Functions section of the PageElement
guide.
State Check Functions
The state check functions of the PageElementMap
class let you check if all
or some of the PageElement
instances managed by a PageElementMap
currently
or eventually have an expected state. They also allow you to wait for some or all
page elements of a map to reach an expected state within a specific timeout.
If a state check function of PageElementMap
requires you to pass the expected
attribute states as a parameter, this parameter needs to be an object whose
keys are taken from the map's mappingObject
and whose values represent the values
of the checked HTML attribute for each PageElement
instance. If you omit
a property representing a certain page element in the parameter object, the
invocation of the state check function will be skipped for this page element.
For state check functions that do not require you to pass the expected attribute
states as a parameter, you can use a filter mask to skip the
invocation of the state check function for certain PageElement
instances.
The following code example demonstrates the usage of the state check functions
of a PageElementList
:
import { stores } from '?/page_objects';
const linkMap = stores.pageNode.ElementMap('//a', {
identifier: {
mappingObject: {
demo: 'demoLink',
examples: 'examplesLink',
api: 'apiLink'
},
mappingFunc: (baseSelector, value) => xpath(baseSelector).id(value)
},
elementOpts: { waitType: Workflo.WaitType.text }
});
// Checks if the text of the `demo` element is 'Demo Page', the text of the `examples`
// element is 'Examples' and the text of the `api` element is 'API'.
linkMap.currently.hasText({
demo: 'Demo Page',
examples: 'Examples',
api: 'API'
});
// Checks if the `demo` and the `api` element of the map currently have any text.
linkMap.currently.hasAnyText({
demo: true,
api: true
});
// Waits for all elements of the map to become visible.
linkMap.wait.isVisible();
// Waits until the `demo` and `examples` elements of the map are not/no longer visible.
linkMap.wait.not.isVisible({ filterMask: {
demo: true,
examples: true,
api: false,
}});
// Checks if the text of the `demo` element does not eventually contain the string 'ap'
// and if the text of the `api` element does not eventually contain the string 'em'.
linkMap.eventually.not.containsText({
demo: 'ap',
api: 'em'
});
To find out how state check functions behave differently when invoked on the
currently
, wait
or eventually
API of a PageElementMap
, please read the
corresponding sections of this guide:
The currently
API,
The wait
API,
The eventually
API.
For more information about the types of available state check functions,
please read the State Check Functions section of the PageElement
guide. Please note that not all types of PageElement
state
check functions are also available on a PageElementMap
.
Filter Masks
The PageElementMap
filter mask allows you to restrict the execution of a
state retrieval, action or state check function to certain managed PageElement
instances.
The PageElementMap
filter mask is an object whose keys are taken from the map's
mappingObject
and whose values are booleans. A property value of true
means
that the function will be executed for the corresponding PageElement
. A value of
false
or simply not defining a property for a certain key means that the
function execution will be skipped.
The filter mask can be set via the last parameter of a state retrieval, action or
state check function. If such a function has other optional parameters, the filter
mask can be defined via the filterMask
property of the opts
parameter (which
is always the last function parameter). Otherwise, the filter mask itself represents
the last function parameter.
Here are some examples for how to use a PageElementMap
filter mask:
import { stores } from '?/page_objects';
const linkMap = stores.pageNode.ElementMap('//a', {
identifier: {
mappingObject: {
demo: 'Demo Page',
examples: 'Examples',
api: 'API'
},
mappingFunc: (baseSelector, value) => xpath(baseSelector).text(value)
}
});
// `texts` will be an object containing two properties, one with the key
// 'demo' and one with the key 'api', whose values are the text of the respective
// page elements. The text of the skipped `examples` page element is not included
// in the result object.
const texts = linkMap.getText({
demo: true,
api: true
});
// Only the `examples` page element will be clicked, because the `api` page element's
// filter mask value is set to `false` and the `demo` page element is not included
// in the filter mask at all.
linkMap.eachDo(
element => element.click(), {
examples: true,
api: false
}
);
// There are other optional parameters like `timeout`, therefore the filter mask
// is defined via the `filterMask` property of the `opts` parameter.
// The `hasAnyText` function will return true if within 3 seconds, the `api`
// page element and the `demo` page element have any text (are not empty).
linkMap.eventually.hasAnyText({ timeout: 3000, filterMask: {
api: true,
examples: false,
demo: true
}});
Filter masks are not available for state check functions that require you to pass
the expected attribute values as a parameter, e.g. hasText(texts)
or
containsValue(values)
. In these cases, you can skip the execution of the state
check function for a certain PageElement
instance by simply not defining
an object property for the corresponding key:
import { stores } from '?/page_objects';
const linkMap = stores.pageNode.ElementMap('//a', {
identifier: {
mappingObject: {
demo: 'Demo Page',
examples: 'Examples',
api: 'API'
},
mappingFunc: (baseSelector, value) => xpath(baseSelector).text(value)
}
});
// The `hasDirectText` function will be invoked for the `demo` and `api` page
// elements of `linkMap` but skipped for the `examples` page element. The function
// returns `true` if the direct text (the text that resides directly/one layer below
// the HTML element) of the `demo` page element is currently 'Demo Page' and the
// direct text of the `api` page element is currently 'API'.
const result = linkMap.currently.hasDirectText({
demo: 'Demo Page',
api: 'API'
});
Waiting Mechanisms
Implicit Waiting
PageElementMap
does not have an implicit waiting mechanism of its own.
However, if you invoke a state retrieval or action function on a PageElement
instance managed by a PageElementMap
, the
implicit waiting mechanism of the PageElement
will be triggered.
The publicly configurable opts
parameter of the ElementMap()
factory method
provides an elementOpts.waitType
property which allows you to define the waitType
of the PageElement
instances managed by the PageElementMap
:
import { stores } from '?/page_objects';
const linkMap = stores.pageNode.ElementMap('//a', {
identifier: {
mappingObject: {
demo: 'demoLink',
examples: 'examplesLink',
api: 'apiLink'
},
mappingFunc: (baseSelector, value) => xpath(baseSelector).id(value)
},
elementOpts: { waitType: Workflo.WaitType.text }
});
linkMap.eachDo(
// The `click()` action function triggers linkElement's implicit waiting mechanism.
// So, before each click, wdio-workflo waits for the linkElement to have any text.
linkElement => linkElement.click()
);
currently
, wait
and eventually
Explicit Waiting: The explicit waiting mechanisms of PageElementMap
are very similar to the
ones used by PageElement
and you should read about them in the
Explicit Waiting
section of the PageElement
guide before you continue reading
this guide.
To learn how the behavior of state retrieval and state check functions of the PageElementMap
class differs from its PageElement
class equivalents, please
read the State Function Types section of this guide.
The types of available state retrieval and state check functions can be
found in the State Function Types section of the PageElement
guide. Please note that not all types of PageElement
state retrieval and state check functions are also available on a PageElementMap
.
currently
API
The The currently
API of the PageElementMap
class consists of state retrieval
functions and state check functions. It does not trigger an implicit wait on the
managed PageElement
instances of the PageElementMap
.
The state retrieval functions of a map's currently
API retrieve the values
of a certain HTML attribute for each PageElement
managed by the map. The return
an object whose keys are taken from the map's mappingObject
and whose values
represent the current values of the retrieved HTML attribute for the respective
page elements.
The state check functions of the currently
API check wether the page elements
managed by the PageElementMap
currently have an expected state for a certain
HTML attribute.
By using a filter mask, you can skip the invocation of a
state retrieval or state check function for certain PageElement
instances of
the map.
wait
API
The Overview
The wait
API of the PageElementMap
class allows you to explicitly wait
for some or all of the map's managed PageElement
instances to have an expected
state. It consists of state check functions only which all return an instance
of the PageElementList
.
If you use a filter mask, the wait
API only waits for the
PageElement
instances included by the filter mask to reach an expected state.
Otherwise, the wait
API waits for all managed PageElement
instances to reach
their expected state. If one or more PageElement
instances fail to reach their
expected state within a specific timeout, an error will be thrown.
Timeout
The timeout
within which the expected states of the PageElement
instances must
be reached applies to each PageElement
instance individually. So, if the timeout
was 3000 milliseconds, each PageElement
instance managed by the map is allowed
to take up to 3 seconds to reach its expected state:
import { stores } from '?/page_objects';
const linkMap = stores.pageNode.ElementMap('//a', {
identifier: {
mappingObject: {
demo: 'demoLink',
examples: 'examplesLink',
api: 'apiLink'
},
mappingFunc: (baseSelector, value) => xpath(baseSelector).id(value)
}
});
linkMap.wait.isVisible({
timeout: 3000,
filterMask: {
demo: true,
api: true
}
});
In the code example above, both the demo
and the api
page element of linkMap
can take up to 3 seconds to become visible. So in total, the maximum possible wait
time for this isVisible
invocation is 6 seconds. If either the demo
, or the api
,
or both page elements do not become visible within 3 seconds, wait.isVisible
will throw an error.
For more information on how to configure the timeout
and interval
of
state check functions defined on the wait
API of a page node class,
please read the wait
API section of the PageElement
guide.
eventually
API
The Overview
The eventually
API of the PageElementMap
class checks if some or all of
the PageElement
instances managed by a PageElementMap
eventually reach an
expected state within a specific timeout. It consists of state check functions only
that return true
if all PageElement
instances for which the state check function
was executed eventually reached the expected state within the specified timeout.
Otherwise, false
will be returned.
If you use a filter mask, the eventually
API only checks the
state of PageElement
instances which are included by the filter mask. Otherwise,
the eventually
API checks the state of all managed PageElement
instances.
Timeout
Like for the wait
API, for the eventually
API too the timeout
within which
the expected states of the PageElement
instances must be reached applies to
each PageElement
instance individually.
For more information on how to configure the timeout
and interval
of
state check functions defined on the eventually
API of a page node class,
please read the eventually
API section of the
PageElement
guide.
ValuePageElementMap
Class
The If you want a map to manage page elements that are derived from the
ValuePageElement
class, you need to use a ValuePageElementMap
instead
of a PageElementMap
.
The ValuePageElementMap
class adds the methods getValue
and setValue
to set and retrieve the values of all page elements managed by the map. Furthermore,
its currently
, wait
and eventually
APIs include the state check functions
hasValue
, containsValue
and hasAnyValue
to wait for or check if some or all
map elements have certain expected values.
Wdio-workflo's example repository contains an Input
class that is derived from
ValuePageElement
. To create a ValuePageElementMap
that manages instances
of the Input
class, the PageNodeStore
of the example repository provides
an InputMap()
factory method.
Below you can find code examples for how to use the getValue
and setValue
methods
as well as the hasValue
, containsValue
and hasAnyValue
state check functions
of our ValuePageElementMap
managing instances of Input
:
const inputMap = stores.pageNode.InputMap('//input', {
identifier: {
mappingObject: {
username: 'username',
password: 'password',
email: 'email'
},
mappingFunc: (baseSelector, value) => xpath(baseSelector).id(value)
}
});
// Performs an implicit wait for each input element of the map and then returns
// the values of the map's input elements as an object whose keys are taken from
// `mappingObject`.
const values: Record<'username' | 'password' | 'email', string> =
inputMap.getValue();
// Returns the values of all input elements managed by the map an object whose
// keys are taken from `mappingObject` withing performing an implicit wait.
const currentValues: Record<'username' | 'password' | 'email', string> =
inputMap.currently.getValue();
// Performs an implicit wait for each map element and then the values of the
// `username` input to 'johnDoe', of the `password` input to 'superSecret' and
// of the `email` input to 'john@doe.com'.
inputMap.setValue({
username: 'johnDoe',
password: 'superSecret',
email: 'john@doe.com'
});
// Performs an implicit wait for the `username` and the `password` input elements
// and then sets the value of the `username` input to 'johnDoe' and the value
// of the `password` input to 'superSecret'.
inputMap.setValue({
username: 'johnDoe',
password: 'superSecret',
});
// Checks if currently, the `username` input has the value 'johnDoe' and the
// `email` input has the value 'john@doe.com'.
inputMap.currently.hasValue({
username: 'johnDoe',
email: 'john@doe.com'
});
// Waits for the `username` input to have the value 'johnDoe', the `password`
// input to have the value 'superSecret' and the `email` input to have the value
// 'john@doe.com' within half a second.
inputMap.wait.not.containsValue({
username: 'johnDoe',
password: 'superSecret',
email: 'john@doe.com'
}, { timeout: 500 });
// Checks if all input elements of `inputMap` eventually have any value
// (are not empty) within the default timeout of our map class.
inputMap.eventually.hasAnyValue();
// Checks if, eventually, the `username` and the `email` input have any value
// (are not empty) within the default timeout of our map class.
inputMap.eventually.hasAnyValue({ filterMask: {
username: true,
email: true,
}});
Unlike the ValuePageElement
class, ValuePageElementMap
is not an abstract
class. The getValue
and setValue
methods of the ValuePageElementMap
are already implemented and internally invoke the getValue
and setValue
methods
of the ValuePageElement
instances managed by the map. Therefore, we can simply use ValuePageElementMap
directly and don't need to create a custom map class just
for the sake of implementing getValue
and setValue
.
We should, however, add a new factory method to our PageNodeStore
that returns
an instance of a ValuePageElementMap
whose types (or rather the types of its
map elements) are already configured. Like in the above code example, where the
InputMap()
factory method returns a ValuePageElementMap
that is configured
to manage instances of the Input
class.
To learn how to create a factory method that returns a configured ValuePageElementMap
, please read the
Adding a map factory method for a custom ValuePageElement
section of the Customizing a Map guide.