Why are page elements questions? #3031
-
|
Hello! I have downloaded serenity-playwright-template and I want to understand why do we consider page elements as questions? |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
|
I love your question, so I'd like to offer you not just an answer, but also my thinking behind it. TL;DR - What are page elements?Page elements are standardised abstractions around web integration tool-specific representations of web elements, such as A construct like import { By, PageElement, Text } from '@serenity-js/web'
export const basketTotal = () =>
PageElement.located(By.css('#basket .total'))
.describedAs('basket total')
export const basketTotalAmount = () =>
Text.of(basketTotal()) // <- Compose PageElement with question about Text
.trim() // <- Compose with a question that trims the result
.replace('£', '') // <- Compose with a question that removes the currency symbol
.as(Number) // <- Compose with a question that converts the result to Number
.describedAs('basket total price') // <- Custom description (optional)The benefit of using Serenity/JS page element abstractions is that you can make your test code agnostic of the underlying web integration code and reuse the same code across projects and teams. But why?To answer why page elements are modelled the way they are, I'd like to share with you my design journey over the past years. The beginningsIn our initial work on the Screenplay Pattern, there were no "page elements"; those came much later. Initially, we had a concept of a "target". We'd use the "target" to encapsulate the description of a web element and its CSS locator. You'll see our early articles from 2016 use "targets" in the code examples like so: // Java
public class ToDoList {
public static Target WHAT_NEEDS_TO_BE_DONE =
Target.the("'What needs to be done?' field")
.locatedBy("#new-todo");
// ...Targets were not part of the Screenplay Pattern and were more of an add-on, sitting somewhere on the sidelines. Serenity/JS v1, released around that time, followed a similar model. The main difference between the first Java and JS implementation was that Serenity/JS supported pluggable Selenium / Protractor selectors rather than being tied to CSS only: // JS
Target.the("'What needs to be done?' field")
.located(by.css("#new-todo")); // or by.xpath, by.id, etc.Even though "targets" served their initial purpose, as teams using both Serenity BDD and Serenity/JS began automating more and more sophisticated websites and web apps, the design limitations began to show. For example, reusing partial selectors relied on string concatenation and token replacement, which felt awkward. Working with lists of elements and picking them based on some criteria was hard. Not to mention that new web integration tools, such as WebdriverIO, have entered the stage, so relying on Selenium-based At that time, I realised that Serenity/JS:
The second attemptIn Serenity/JS v2, I created two web integration packages - In a Protractor test, we'd have: import { Target } from '@serenity-js/protractor';
import { by } from 'protractor';
const basketItems = Target.all('items in the basket').located(by.css('ul#basket li'))In a WebdriverIO test, we'd see: import { by, Target } from '@serenity-js/webdriverio';
const basketItems = Target.all('items in the basket').located(by.css('ul#basket li'))The code for each web integration tool would be nearly identical, save for the imports. What it meant is that migrating from Protractor to WebdriverIO would require you to simply find & replace your import statements rather than rewrite your entire test suite. That helped a lot of teams. The other notable addition was that the Serenity/JS v2 target model allowed for easier filtering based on pluggable criteria. For example: const basketItems = Target.all('items in the basket')
.located(by.css('ul#basket li'))
.where(Text, endsWith('e'))
.first() While much more useful than my first implementation, "targets" were still missing something to become truly portable: Serenity/JS v2 did not implement targets as "questions". They were elegant builders that under the hood resolved to But then Playwright came about... At that time, I came to the following realisation:
Abandoning the targetsWith Serenity/JS v3, I decided to depart from the original "target" model altogether. To address the first challenge, I created Serenity/JS However, abstracting the selectors required me to abstract the representation of web elements as well. I also realised that a "page element" is closely related to a "question", but is not a question itself. If it was a question, what would that question resolve to? Probably an
The introduction of standardised It also enabled test code reuse across web integration tools. For example, you can write a web component test using Serenity/JS + Playwright Component Test Runner, and then plug your test code into an end-to-end test suite using Serenity/JS + WebdriverIO and running against a remote Selenium grid. I hope this story has helped you to understand how and why I arrived at the current design. It might have also gave you hints of where I might take it next ;) |
Beta Was this translation helpful? Give feedback.

I love your question, so I'd like to offer you not just an answer, but also my thinking behind it.
TL;DR - What are page elements?
Page elements are standardised abstractions around web integration tool-specific representations of web elements, such as
Elementin Selenium-based tools orLocatorin Playwright.A construct like
PageElement.located(By.css('...'))returns a composableQuestionimplementation (aMetaQuestionAdapterto be precise) that allows for composing page elements together, or mapping them to transform their value.