Howto Add New Methods to a Navigator?

228 views
Skip to first unread message

Bob Brown

unread,
Oct 11, 2021, 9:36:35 PM10/11/21
to geb-...@googlegroups.com

I am trying to make life a bit easier for my userbase. I allow things like:

 

===

       then: "Establish Contact Details"

        withFlowViewForm('contactDetailsCheck') { form ->

            form.noPhone = true

            form.anonymous = true

            form.clickCustomRadioButtonHavingNameAndText('caller', cfg.caller)

 

            form.clickNextButton()

        }

===

 

Obviously, clickCustomRadioButtonHavingNameAndText and clickNextButton are not standard Geb/Selenium methods…

 

Inside ‘withFlowViewForm’, I add clickCustomRadioButtonHavingNameAndText, clickNextButton (and more) closures to the ‘form’ INSTANCE metaClass:

 

===

   void withFlowViewForm(view, c) {

        withFrame('iframe') {

 

            final form = $('form')

 

            // inefficient, but simple and works

            form.metaClass.clickNextButton = { clickButton(delegate, 'next') }

 

            form.metaClass.clickCustomRadioButtonHavingNameAndText = { name, text ->

                def radio = delegate.find('label.form-check-label', text: text).parent().find('input', name: name).module(RadioButtons)

                if (radio.isEmpty())

                    throw new LightweightException("Could not find radio button with {name: ${name}, text: ${text}}")

                radio.click()

            }

 

            c(form)

        }

    }

 

    private void clickButton(form, evt) {

        report "${form.parent('body').attr('id')}"

        def btn = form."_eventId_${evt}"()

        if (btn.isEmpty())

            throw new LightweightException("Could not find button with {attribute: _eventId_${evt}}")

        btn.click()

    }

===

 

This works very nicely, but I can’t help but feel that there should be a more efficient way of doing this…ideally, I would like to decorate ‘form’ one time only at startup (perhaps in GebConfig?).

 

I have read the doco section: “7.2.2. Navigator factory” is this the way to go? Provide an extension of Navigator?

I like that the doco says “..get in touch via the mailing list if you need help.”

 

Here I am 😊

 

Can anybody guide me?

 

Thanks,

 

BOB

 

J. David Beutel

unread,
Oct 11, 2021, 10:21:11 PM10/11/21
to geb-...@googlegroups.com
Have you tried using page objects for this?

https://gebish.org/manual/5.0/#pages

Cheers,
11011011
--
You received this message because you are subscribed to the Google Groups "Geb User Mailing List" group.
To unsubscribe from this group and stop receiving emails from it, send an email to geb-user+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/geb-user/SY4P282MB1883F0917C84AD9F3210D85EEDB69%40SY4P282MB1883.AUSP282.PROD.OUTLOOK.COM.

Bob Brown

unread,
Oct 11, 2021, 11:43:22 PM10/11/21
to geb-...@googlegroups.com

Thanks for the response.

 

I didn’t try page objects for this.

I’m just letting the users do basic scripting…they seem to be happy with that and I don’t think that they would cope too well with too much ‘rigor’…

 

Wouldn’t a page also decorate each instance? If so it is probably just a potentially cleaner equivalent to what I am already doing?

 

Also not too sure how to use a page in this situation!

 

The doco does say “While not as crosscutting as using a custom navigator factory it’s also possible to decorate navigators with additional methods by creating modules based on them.”

 

So maybe:

 

final form = $('form').module(MyModuleWithMethodsDefined)

 

Still on a per-instance basis but this would probably be lighter-weight than all the metaClass stuff I am doing now.

 

Worth trying???

 

BOB

Bob Brown

unread,
Oct 12, 2021, 4:15:13 AM10/12/21
to geb-...@googlegroups.com

Well…that was interesting!

 

Here’s where I have ended up:

 

===

    void withFlowViewForm(view, c) {

        withFrame('iframe') {

            //            Useful for debugging:

            //                println $('body').getAttribute("outerHTML")

 

            final id = $('body').@id

            waitFor(message: "Could not find body with id==${view}, found: ${id}") { id == view }

 

            c($('form').module(new FlowFormDecoratorModule(id: id, report: { msg -> report msg })))

        }

    }

 

 

class FlowFormDecoratorModule extends Module {

    String id

    def report

 

    def clickNextButton() { clickButton(navigator, 'next') }

 

    [...elided..]

 

    private void clickButton(form, evt) {

        report id

        def btn = form."_eventId_${evt}"()

        if (btn.isEmpty())

            throw new LightweightException("Could not find button with {attribute: _eventId_${evt}}")

        btn.click()

    }

}

===

 

I ASSUME(/hope) that this is more ‘performant’ than the metaClass-hacking that I did before(…?)

It’s a bit cleaner code, so that’s a ‘win’, regardless…

 

The one ‘wrinkle’ I found: if I used ‘browser.report’ within FlowFormDecoratorModule, it produced different results to when I used ‘report’ from within withFlowViewForm.

 

This may have something to do with the fact that I am also using geb-spock-reports.

 

Outside the module (directly within withFlowViewForm for example) I would end up with a PNG image named very specifically, and based on more than just the report parameter value like: 001-002-EXXXXX__Cold_Water__Reason_For_Call__Normal_Path-fred.png.

 

Within the module, I couldn’t just call ‘report’ and if I called “browser.report ‘fred’”, I would simply get ‘fred.PNG (which didn’t play well with geb-spock-reports).

 

That’s why I pass a ‘report’ closure as parameter to the module: it lets me use the ‘good’ report instance…

 

Thanks to J. David Beutel for planting the seed…

 

BOB

Marcin Erdmann

unread,
Oct 12, 2021, 3:59:11 PM10/12/21
to geb-...@googlegroups.com
On 2021-10-11 15:36 , Bob Brown wrote:

I have read the doco section: “7.2.2. Navigator factory” is this the way to go? Provide an extension of Navigator?

I like that the doco says “..get in touch via the mailing list if you need help.”

 

Here I am 😊


LOL

The doco does say “While not as crosscutting as using a custom navigator factory it’s also possible to decorate navigators with additional methods by creating modules based on them.”
 
So maybe:
 
final form = $('form').module(MyModuleWithMethodsDefined)

Yes, this would be my preference - use a module to decorate a navigator. The biggest benefit in my eyes is that IntelliJ is able to infer types correctly and you get autocompletion/navigation to the implementation for methods added this way while adding them via meta classes or custom navigator factory makes everything dynamic and opaque to IntelliJ thus significantly hindering authoring of your Geb code. Custom navigator factories are a mechanism for this predating addition of Navigator.module() method (in https://github.com/geb/issues/issues/311) which allows to create instances of modules anywhere and not just in content blocks of other modules and pages as it was the case before. That in turn made it much simpler and concise to decorate a navigator instance with methods defined on a module.


On Tue, Oct 12, 2021 at 9:15 AM Bob Brown <b...@transentia.com.au> wrote:

I ASSUME(/hope) that this is more ‘performant’ than the metaClass-hacking that I did before(…?)

It’s a bit cleaner code, so that’s a ‘win’, regardless…


I'm not sure it's that much more "performant" but it wouldn't be me immediate concern. It's definitely cleaner and easier to understand code. And as I said, there are benefits to authoring code using such methods if they are added that way.
 

 

The one ‘wrinkle’ I found: if I used ‘browser.report’ within FlowFormDecoratorModule, it produced different results to when I used ‘report’ from within withFlowViewForm.

 

This may have something to do with the fact that I am also using geb-spock-reports.

 

Outside the module (directly within withFlowViewForm for example) I would end up with a PNG image named very specifically, and based on more than just the report parameter value like: 001-002-EXXXXX__Cold_Water__Reason_For_Call__Normal_Path-fred.png.

 

Within the module, I couldn’t just call ‘report’ and if I called “browser.report ‘fred’”, I would simply get ‘fred.PNG (which didn’t play well with geb-spock-reports).

 

That’s why I pass a ‘report’ closure as parameter to the module: it lets me use the ‘good’ report instance…


This is because outside of the module you are not calling Browser.report(), you are actually calling GebReportingSpec.report() which prepends the report name with some counters and the name of the current test. If you're on Geb 5 then you could pass testManager to your module and call report() on that - testManager is an instance of geb.test.GebTestManager and GebReportingSpec actually delegates the report() method to it.
 

 

Thanks to J. David Beutel for planting the seed…


And thanks to you for reading the docs - quite a lot of effort went into them over the years and it goods to see people making use of them!

Marcin

Bob Brown

unread,
Oct 12, 2021, 9:10:43 PM10/12/21
to geb-...@googlegroups.com

That’s why I pass a ‘report’ closure as parameter to the module: it lets me use the ‘good’ report instance…

 

This is because outside of the module you are not calling Browser.report(), you are actually calling GebReportingSpec.report() which prepends the report name with some counters and the name of the current test. If you're on Geb 5 then you could pass testManager to your module and call report() on that - testManager is an instance of geb.test.GebTestManager and GebReportingSpec actually delegates the report() method to it.

 

Nice background info and suggestion. Worked like a charm. Thanks.

 

BOB

Reply all
Reply to author
Forward
0 new messages