Geb / Groovy - Page content not recognized when used within other Page Object method - groovy

When I click a button I have to wait for some dynamic content to be rendered. When I put the waitFor closure in the test it works correctly. However, I wanted to put the waitFor in a method inside the Page object so I do not have to always call the waitFor after every click, but when I do that it fails stating it cannot find the property.
This does not work:
class LandingPage extends Page {
static content = {
resultsBtn(to: ResultsPage) { $("button", id: "showresults") }
}
void getResults() {
resultsBtn.click()
waitFor { ResultsPage.results.displayed }
}
}
class ResultsPage extends Page {
static content = {
results { $("div", id: "listresults") }
}
}
class ShowResults extends GebReportingTest {
#Test
public void displayResults() {
to LandingPage
getResults()
}
}
The error states something like "No such property: results for class ResultsPage".
Is it possible to put references to content from other Page Objects inside other Page Object methods?
EDIT: I feel like this is more of a Groovy specific thing rather than Geb. I'm not sure if it's even possible to access bindings within the content closure. But it also seems like creating a getVariable() function inside the Page Object doesn't help much either.

First you shouldn't assign closures in content blocks (there's unnecessary = in ResultPage) but pass them to an implicit method, you should have:
static content = {
results { $("div", id: "listresults") }
}
The other question is why do you want to model this as two pages? As far as I understand clicking the button doesn't cause a page reload but there's an ajax call to retrieve the results. I would simply put both results and resultsBtn as contents of one page and your problem would be gone.
EDIT:
It turns out that a page change is involved in your case. Assuming that you always want to wait for these results to appear you can either:
put your waitFor condition inside of static at = {} block for ResultsPage - at checks are executed implicitly whenever you use to() which means that it will wait wherever you go to that page
put a waitFor in a page change listener
access current page via the browser property on a page, in LandingPage: waitFor { browser.page.results.displayed } but this seems like a dirty solution to me - reaching from one page to another...

Related

How do I get a parameter to not just display in a component, but also be recognized inside of OnInitializedAsync()?

I'm working on a blazor server-side project and I have a component that gets passed a model (pickedWeek) as a parameter. I can use the model fine in-line with the html, but OnInitializedAsync always thinks that the model is null.
I have passed native types in as parameters, from the Page into a component, this way without an issue. I use a NullWeek as a default parameter, so the number getting used in OnInitializedAsync only ever appears to be from the NullWeek. In case this is related, there is a sibling component that is returning the Week model to the Page through an .InvokeAsync call, where StateHasChanged() is being called after the update. It appears that the new Week is getting updated on the problem component, but that OnInitializeAsync() either doesn't see it, or just never fires again- which maybe is my problem, but I didn't think it worked that way.
For instance, the below code will always show "FAILURE" but it will show the correct Week.Number. Code below:
<div>#pickedWeek.Number</div>
#if(dataFromService != null)
{
<div>SUCCESS</div>
}
else
{
<div>FAILURE</div>
}
#code{
[Parameter]
public Week pickedWeek { get; set; }
protected IEnumerable<AnotherModel> dataFromService { get; set; }
protected override async Task OnInitializedAsync()
{
if (pickedWeek.Number > 0)
{
dataFromService = await _injectedService.MakeACall(pickedWeek.Id);
}
}
}
#robsta has this correct in the comments, you can use OnParametersSet for this. Then, you will run into another issue, in that each rerender will set your parameters again and generate another call to your service. I've gotten around this by using a flag field along with the the OnParametersSet method. Give this a shot and report back.
private bool firstRender = true;
protected override async Task OnParametersSetAsync()
{
if (pickedWeek.Number > 0 && firstRender)
{
dataFromService = await _injectedService.MakeACall(pickedWeek.Id);
firstRender = false;
// MAYBE call this if it doesn't work without
StateHasChanged();
}
}
Another alternative is to use the OnAfterRender override, which supplies a firstRender bool in the the method signature, and you can do similar logic. I tend to prefer the first way though, as this second way allows it to render, THEN sets the value of your list, THEN causes another rerender, which seems like more chatter than is needed to me. However if your task is long running, use this second version and build up a loading message to display while the list is null, and another to display if the service call fails. "FAILURE" is a bit misleading as you have it as it's being displayed before the call completes.
I've also found that a call to await Task.Delay(1); placed before your service call can be useful in that it breaks the UI thread loose from the service call awaiter and allows your app to render in a loading state until the data comes back.

Is it possible to look within the at element in pages?

is it possible to look for elements only within the "At-Element" ?
Example Page:
class SearchDialogPage extends Page {
static at = { $('div', class: 'modalOverlay').has('div', class: 'contentbox__title', text: 'Search for Company') }
static content = {
nameTextline { $('div').has('label', text:'Name').$('input') module TextInput }
}
} }
I find more than one element for nameTextline, so i want to tell the Page, that it has to look into the div-Element declared in the "at" field.
Semantically, at is not a content element but a boolean condition, i.e. whatever code you have inside there will be evaluated as a "Groovy truthy" value. You should define your element in the content section, then refer to it from your at condition, not the other way around.

Call LitElement method from outside the element

I have a simple LitElement component like so:
class MyElement extends LitElement {
constructor() {
super();
}
customMethod(data) {
// do something with the passed parameter
}
render() {
return html`<div id="element"></div>`;
}
}
customElements.define('my-element', MyElement);
And I want to be able to call that customMethod from outside of my element.
So for example if I add the element to web page like so:
<my-element></my-element>
I then want to be able to add some JavaScript to the page and call that customMethod.
I tried:
var element = document.getElementById('element');
element.shadowRoot.customMethod('example data');
But it claims it's not available... How can I call a method on an instance of LitElement?
You don't need to use shadowRoot in the call :
var element = document.getElementById('element');
element.customMethod('example data');
but you need to be able to locate your element
<my-element id='element'></my-element>
I had a very similar problem and the existing answers did not seem to fix it. The reason for my issue was caused by the fact that LIT Element scripts are exported as modules, meaning that they are loaded and executed after the initial DOM has been parsed. So if you are using a script to access the public method - make sure that it is also in a module (or you can alternatively place the code into an appropriate timeout).
So when defining an element in LIT Element as follows:
#customElement('my-element')
export class MyElement extends LitElement {
#state()
text = '';
customMethod(data) {
this.text = 'Custom method was called!';
}
render() {
return html`<div id="element">${this.text}</div>`;
}
}
And adding a script in my index.html page:
<my-element id='element'></my-element>
<script type="module">
const element = document.getElementById('element');
element.customMethod();
</script>
Make sure that the script tag contains type="module". Otherwise you will see the following error in the console: Uncaught TypeError: element.customMethod is not a function
Link to LIT Element Playground.
Also, here is a great article that explains how scripts are loaded in detail.

Is there a way in Geb to automatically assign the right Module to all Elements in a Form

We use Geb to run our Frontend Tests and we have some quite complex pages in our application.
Some of the pages have forms with a lot of different buttons, checkboxes and some multiselects.
I love the feature of geb/groovy that i just have to define the form in the Page Object and then can access all its elements in it.
static content = {
form { $("#form")}
}
But for them to be clickable and to query if they are readonly and more they need to be at least of type FormElement which does not happen with the above method. So I have to mention all these FormElements separately:
static content = {
form { $("#form")}
button1 { $("#button1").module(FormElement)}
button2 { $("#button2").module(FormElement)}
checkbox{ $("#checkbox").module(Checkbox)}
...
}
All those buttons, checkboxes... are already in the form variable, but cannot be clicked or checked if they are selected and so on. It's also not possible to apply the the module afterwards like this:
def "test something"() {
when:
form.button1.module(FormElement).click() //error
then:
...
}
Is there no way to automatically assign each input, checkbox, radiobutton, button,... the correct Module based on their type without the need of doing it by hand?
If someone could also point me in the right direction to understand how this "form { $("#form")}" works, that i can access all sub elements by its name by just suppying the form, that would be nice!
For your example of creating a module based on a form control you need to obtain a navigator for the control and not it's value. It's done by calling a method named the same as the control you're trying to access (it's explained in this section of The Book of Geb):
form.button1().module(FormElement).click()
If you want to automatically create modules based on the element type then you could create a Module for the form and override method missing:
class FormModule extends Module {
Object methodMissing(String name, Object args) {
def result = super.methodMissing(name, args)
if (result instanceof Navigator && result.tag() == "input") {
switch (result.#type) {
case "checkbox":
result = result.module(Checkbox)
break
default:
result = result.module(FormElement)
}
}
result
}
}
then you would use it like:
static content = {
form { $("#form").module(FormModule) }
}
form.button1().click()

General Problems With Geb (StaleElementReferenceException & Wait Timeouts)

According to the "Book of Geb" I started to map our portal's web pages. I prefer to use variables defined within static content closure block and accessing them afterwards in page methods:
static content = {
buttonSend { $("input", type: "submit", nicetitle: "Senden") }
}
def sendLetter() {
waitFor { buttonSend.isDisplayed() }
buttonSend.click()
}
Unfortunately, sometimes I get an Geb waiting timeout exception (after 60 secs) or even worse I receive the well known "StaleElementReferenceException".
I could avoid the wait timeout when using "isEnabled" instead of "isDisplayed" but for the "StaleElementReferenceException" I could only apply the below solution:
def sendLetter() {
waitFor { buttonSend.isEnabled() }
try {
buttonSend.click()
} catch (StaleElementReferenceException e) {
log.info(e.getMessage())
buttonSend.click()
}
}
I guess, this solution is not really nice but I could not apply an explicitly wait as described in another article. Thus, I have some general questions:
Should I avoid to use static content definitions when pages are dynamically?
At what time or event Geb is refreshing its DOM? How can I trigger the DOM refreshment?
Why I still get a "StaleElementReferenceException" when using CSS selectors?
I would appreciate every hint which helps to understand or to solve this issue. The best would be to have a simple code example since I'm still a beginner. Thank you!
If you defined an at check on your page class the page would first verify that condition and wait for the first n seconds. Which is assigned in your gebConfig file. The default is 30 seconds.
static at = {
waitFor { buttonSend.isDisplayed() }
}
Thus once you call your pages 'to' method with a test or whatever you are using it for the page will wait and then perform your page manipulations.
to MyPage
buttonSend.click()
Should I avoid to use static content definitions when pages are dynamically?
No. Actually, the static definitions are of closures. So what is
actually happening is each time you make use of that Pages static
components you are calling a closure which is run dynamically on the
current page(collection of webElements). Understanding this is key to
using Geb and discovering the problems you will run into.
At what time or event Geb is refreshing its DOM? How can I trigger the DOM refreshment?
When you call: to, go, at, click ,withFrame(frame, page), withWindow
and browser drive methods it will refresh the current set of
WebElements. Geb has a nice collection of utiliities to make switching
between pages and waiting for page manipulations easy. Note: Geb is
actually built on WebDriver WebElements.
Why I still get a "StaleElementReferenceException" when using CSS selectors?
It is possible the page hasn't finished loading, has been manipulated
with ajax calls or has been refreshed in some other way. Sometimes an
'at' PAGE method call can fix these issues. They are for me most
common when using frames as Geb seems to become confused between pages
and frames a little. There are workarounds.
In short if you use the page pattern you can easily switch expected pages using the Page class you have defined with a static content, at, and url closure using the below:
to(Page)
at(Page)
Navigator.click(Page)
withFrame(frame, Page) { }
In addition to twinj's answer, I would like to point out a couple of other workarounds in case you encounter a StaleElementReferenceException.
Often times I find it is better to write out your selector manually rather than rely on the contents as defined in the page. Even though your page contents should not be cached by default, they still manage to slip away from me at times. This is particularly prevalent when dealing with dynamic content or iterations.
Ex: Let's say we want to click an element from a dynamically created dropdown.
Typically you might want to do something like...
static content = {
dropdown { $("#parentDiv").find("ul") }
}
void clickDesiredElement(String elementName) {
dropdown.click()
def desiredElement = dropdown.find("li", text:elementName)
waitFor { desiredElement.displayed }
desiredElement.click()
}
If this doesn't work, try getting rid of the contents altogether, and writing out the selector manually...
void clickDesiredElement(String elementName) {
$("#parentDiv").find("ul").click()
def desiredElement = $("#parentDiv").find("ul").find("li", text:elementName)
waitFor { desiredElement.displayed }
desiredElement.click()
}
In really nasty cases, you may have to use a manual timer, as pointed out in this answer, and your code may look like this...
void clickDesiredElement(String elementName) {
$("#parentDiv").find("ul").click()
sleepForNSeconds(2)
def desiredElement = $("#parentDiv").find("ul").find("li", text:elementName)
waitFor { desiredElement.displayed }
desiredElement.click()
}
Keep in mind this is a workaround :)
For large iterations and convenient closure methods, such as each{} or collect{}, you may want to add a waitFor{} in each iteration.
Ex: Let's say we want to get all rows of a large table
Typically you might want to do something like...
def rows = $("#table1").find("tr").collect {
[
name: it.find("td",0),
email: it.find("td",1)
]
}
Sometimes I find myself having to do this iteratively, along with a waitFor{} between each iteration in order to avoid a StaleElementReferentException. It might look something like this...
def rows = []
int numRows = $("#table1").find("tr").size()
int i
for(i=0; i < numRows; i++) {
waitFor {
def row = $("#table1").find("tr",i)
rows << [
name: row.find("td",0),
email: row.find("td",1)
]
}
}
I have figured that it is the navigator which get lost when you load dynamically.
I've solve the issue locally by reinit the page or module with below code:
void waitForDynamically(Double timeout = 20, Closure closure) {
closure.resolveStrategy = Closure.DELEGATE_FIRST
switch (this) {
case Module:
init(browser, browser.navigatorFactory)
break
case Page:
init(browser)
break
default:
throw new UnsupportedOperationException()
}
waitFor {
closure()
}
}

Resources