get access to driver in AbstractGlobalExtension

56 views
Skip to first unread message

GebUser

unread,
Dec 21, 2020, 4:56:16 PM12/21/20
to Geb User Mailing List
hi Marcin,
I am using a class test status tracker class that extends AbstractGlobalExtension.

When an error occurs, I need to take a screenshot. Can you please tell how I can access the browser driver here?

class TestResultExtension extends
AbstractGlobalExtension
{
@Override
void visitSpec(SpecInfo spec) {
spec.addListener(new ErrorListener())
}

static class ErrorListener extends AbstractRunListener {
ErrorInfo errorInfo
# Browser browser

@Override
void beforeIteration(IterationInfo iteration) {
errorInfo = null
}

void afterIteration(IterationInfo iteration){
if (errorInfo!=null){
driver.manage().window().maximize()
takeScreenshot(driver)
}
}

Alexander Kriegisch

unread,
Dec 21, 2020, 8:23:55 PM12/21/20
to geb-...@googlegroups.com
Hi.

I hope it is okay, even though I am not Marcin. ;-)

It looks like you you found the sample code in my SO answer here:
https://stackoverflow.com/a/50679606/1082681

Before we continue, making a possibly simple thing complicated: Are you
aware of the fact that if your specification extends GebReportingSpec,
it will by default have a screenshot reporter, so by a statement like
"report 'my screenshot'" you can just take a screenshot and it will
automatically take a screenshot if the test fails?

See https://gebish.org/manual/current/#testing-reporting and also search
the Book of Geb for "reporting" or "reporter". Just lately someone here
in this group posted about how to write a custom reporter in order to
use AShot for full page screenshot (if the page is bigger than the
viewport or even bigger than the screen size).

Now, having the basic information out of the way and assuming for a
minute that the default screenshot reporter or even a custom reporter
are inadequate for you (which I don't believe, BTW), here is how from
your global Spock extension's RunListener you could in principle access
browser and driver: First you need to get access to the specification
instance, then check if it is really a Geb specification (because not
every Spock spec is a Geb spec) and if so, you can use its methods in
order to access browser and driver:

------------------------------------------------------------------------

package de.scrum_master.testing.extension

import geb.spock.GebSpec
import org.spockframework.runtime.AbstractRunListener
import org.spockframework.runtime.extension.AbstractGlobalExtension
import org.spockframework.runtime.model.ErrorInfo
import org.spockframework.runtime.model.IterationInfo
import org.spockframework.runtime.model.SpecInfo
/**
* See https://stackoverflow.com/a/50679606/1082681
*/
class TestResultExtension extends AbstractGlobalExtension {
@Override
void visitSpec(SpecInfo spec) {
spec.addListener(new ErrorListener())
}

static class ErrorListener extends AbstractRunListener {
ErrorInfo errorInfo

@Override
void beforeIteration(IterationInfo iteration) {
errorInfo = null
}

@Override
void error(ErrorInfo error) {
errorInfo = error
}

@Override
void afterIteration(IterationInfo iteration) {
if (iteration.feature.spec instanceof GebSpec) {
def driver = (iteration.feature.spec as GebSpec).driver
driver.manage().window().maximize()
takeScreenshot(driver)
}
}

}
}

------------------------------------------------------------------------

BTW, you could also check if the spec is an instance of GebReportingSpec
and directly use the reporting feature there. But really, what you
probably want is just use the plain vanilla GebReportingSpec. Maybe,
maybe you want to use a custom reporter in combination with that.

Friendly regards
--
Alexander Kriegisch
https://scrum-master.de

GebUser

unread,
Jan 4, 2021, 11:20:55 AM1/4/21
to Geb User Mailing List
where is this "driver" coming from?
def driver = (iteration.feature.spec as GebSpec).driver  

I get this error
Cannot cast object 'org.spockframework.runtime.model.SpecInfo@73f38e62' with class 'org.spockframework.runtime.model.SpecInfo' to class 'geb.spock.GebSpec'

Marcin Erdmann

unread,
Jan 4, 2021, 4:10:41 PM1/4/21
to geb-...@googlegroups.com
Unfortunately the code provided by Alexander is not quite right. You cannot obtain an instance of the spec in an AbstractRunListener because it's not available on IterationInfo. You will need to combine an AbstractRunListener with an IMethodInterceptor, like geb.spock.OnFailureReporter which ships with Geb does. The adapted example from Alexander would look like this:

import geb.spock.GebSpec
import org.spockframework.runtime.AbstractRunListener
import org.spockframework.runtime.extension.AbstractGlobalExtension
import org.spockframework.runtime.extension.IMethodInterceptor
import org.spockframework.runtime.extension.IMethodInvocation
import org.spockframework.runtime.model.ErrorInfo
import org.spockframework.runtime.model.SpecInfo


class TestResultExtension extends AbstractGlobalExtension {
    @Override
    void visitSpec(SpecInfo spec) {
        def reporter = new ErrorReporter()
        spec.addListener(reporter)
        spec.allFeatures*.addIterationInterceptor(reporter)
    }

    static class ErrorReporter extends AbstractRunListener implements IMethodInterceptor {

        GebSpec specInstance

        @Override
        void error(ErrorInfo error) {
            if (specInstance) {
                def driver = specInstance.driver
                driver.manage().window().maximize()
                takeScreenshot(driver)
            }
        }

        @Override
        void intercept(IMethodInvocation invocation) throws Throwable {
            def specInstance = invocation.instance
            if (specInstance instanceof GebSpec) {
                this.specInstance = specInstance
            }

            try {
                invocation.proceed()
            } finally {
                this.specInstance = null
            }
        }
    }
}

--
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/bc7bbb02-6f6c-46f0-bd0f-45985f9eae15n%40googlegroups.com.

Alexander Kriegisch

unread,
Jan 4, 2021, 8:41:12 PM1/4/21
to geb-...@googlegroups.com
>> I get this error
>>
>> Cannot cast object 'org.spockframework.runtime.model.SpecInfo@73f38e62'
>> with class 'org.spockframework.runtime.model.SpecInfo' to class
>> 'geb.spock.GebSpec'

The simple answer is: I was sloppy and wish too apologise for it. This
is the perfect example why I keep telling the teams I coach: "If you are
sloppy, you either have to do it twice or someone else has to clean up
after you." The latter was the case: Marcin cleaned up after me and I am
feeling embarrassed. So in order to learn a lesson from this, let me
describe what happened:

-- I added the 'afterIteration' method to my existing extension for
which also a test using it exists.
-- The existing test is a simple Spock test, not a Geb test.
-- Looking at 'if (iteration.feature.spec instanceof GebSpec)', first
of all there is a bug because despite the suggestive property name
'spec' (which in reality is a method name 'getSpec') the return
value is an instance of SpecInfo, not of Specification. Maybe the
getter should have been named 'getSpecInfo', but anyway it was my
mistake. I did not actually run or debug is, just quickly hacked it
into my IDE using code completion. Secondly, even if this piece of
code would have been correct, I do not really have a GebSpec
running this extension which would make the code enter the 'if'
block. Double mess-up!
-- I actually never planned to run the code because I do not have
access to your 'takeScreenshot' method, simply because you did not
provide it in your original question. So for me it was pseudo code
and I was too lazy to create a dummy 'takeScreenshot' method just
logging something or whatever.

I wanted to save time and just help getting you started. My main
objective was to convince you to try GebReportingSpec anyway, like I
said. But the sample code was just sloppy. Thanks to Marcin for cleaning
up my mess!

--
Alexander Kriegisch
https://scrum-master.de


>> <mailto:geb-user+u...@googlegroups.com> .
>> <https://groups.google.com/d/msgid/geb-user/bc7bbb02-6f6c-46f0-bd0f-45985f9eae15n%40googlegroups.com?utm_medium=email&utm_source=footer>
>> .
>
> --
> 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
> <mailto:geb-user+u...@googlegroups.com> .
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/geb-user/CA%2B52dQR7c26w6HwkE3boaA6_BSV7NqypUd_62juyH%3DA6GewT%3Dg%40mail.gmail.com
> <https://groups.google.com/d/msgid/geb-user/CA%2B52dQR7c26w6HwkE3boaA6_BSV7NqypUd_62juyH%3DA6GewT%3Dg%40mail.gmail.com?utm_medium=email&utm_source=footer>
> .

Alexander Kriegisch

unread,
Jan 4, 2021, 9:28:19 PM1/4/21
to geb-...@googlegroups.com
I forgot to mention: A SpecInfo is just meta information, the best you
can get from there is the spec class in order wo rewrite the 'if' as

if (GebSpec.isAssignableFrom(iteration.feature.spec.reflection))

But then you are at a dead end if you need the actual specification
instance because you cannot get it, so there is no way to access its
fields or methods like 'getDriver()' from there either. This is why
Marcin's approach to use an IMethodInterceptor is exactly right. From
the 'IMethodInvocation invocation' parameter you can easily get the
instance, while from an 'IterationInfo iteration' you cannot. So you
see, my quick & dirty hack without running the code is fundamentally
wrong on several levels.
--
Alexander Kriegisch
https://scrum-master.de


> to geb-user+u...@googlegroups.com.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/geb-user/20210105014110.532C33380703%40dd39516.kasserver.com.
>
>

Alexander Kriegisch

unread,
Jan 5, 2021, 12:10:09 AM1/5/21
to geb-...@googlegroups.com

Okay, here is a slightly improved version of what Marcin suggested. It only attaches run listener + method interceptor to Spock specs which are actual Geb specs, reducing runtime overhead. It also contains a sample method for taking a screenshot, just to be able to run my sample test.


package de.scrum_master.testing.extension

import geb.spock.GebSpec
import org.openqa.selenium.OutputType
import org.openqa.selenium.TakesScreenshot
import org.openqa.selenium.WebDriver

import org.spockframework.runtime.AbstractRunListener
import org.spockframework.runtime.extension.AbstractGlobalExtension
import org.spockframework.runtime.extension.IMethodInterceptor
import org.spockframework.runtime.extension.IMethodInvocation
import org.spockframework.runtime.model.ErrorInfo
import org.spockframework.runtime.model.SpecInfo

import java.nio.file.Files

/**
* See https://groups.google.com/g/geb-user/c/vrNZFaRyDQQ
*/
class TestFailureScreenshotExtension extends AbstractGlobalExtension {
@Override
void visitSpec(SpecInfo spec) {
if (GebSpec.isAssignableFrom(spec.reflection)) {
def reporter = new ScreenshotOnErrorReporter()
spec.addListener(reporter)
spec.allFeatures*.addIterationInterceptor(reporter)
}
}

static class ScreenshotOnErrorReporter extends AbstractRunListener implements IMethodInterceptor {
GebSpec gebSpec


@Override
void intercept(IMethodInvocation invocation) throws Throwable {
      gebSpec = invocation.instance
try {
invocation.proceed()
} finally {
gebSpec = null
}
}

@Override
void error(ErrorInfo error) {
def driver = gebSpec.driver
driver.manage().window().maximize()
takeScreenshot(driver)
}

void takeScreenshot(WebDriver driver) {
// Not all WebDrivers can take screenshots, e.g. HtmlUnitDriver
if (!(driver instanceof TakesScreenshot)) {
println "Driver $driver is incapable of taking screenshots"
return
}
def screenshotTempFile = (driver as TakesScreenshot).getScreenshotAs(OutputType.FILE)
def targetDir = new File(gebSpec.browser.reportGroupDir, gebSpec.class.name.replaceAll("[.]", "/"))
// TODO: generate nicer name for screenshot file, this is just an example
def screenshotFile = new File(targetDir, System.nanoTime() + ".png")
targetDir.mkdirs()
Files.copy(screenshotTempFile.toPath(), screenshotFile.toPath())
println "Screenshot saved as $screenshotFile"
}
}

}

package de.scrum_master.testing.extension

import geb.spock.GebSpec
import spock.lang.Ignore
import spock.lang.Unroll

//@Ignore("test fails on purpose; activate if you want to see screenshots taken via global extension")
class TestFailureGebReportingTest extends GebSpec {
static url = this.getResource("/simple-form-page.html").toString()

def "failing normal feature"() {
given:
go url

expect:
0 == 1
}

def "passing normal feature"() {
given:
go url

expect:
0 == 0
}

def "parametrised feature"() {
given:
go url

expect:
a == b

where:
a << [2, 4, 6]
b << [3, 5, 6]
}

@Unroll
def "unrolled feature with #a/#b"() {
given:
go url

expect:
a == b

where:
a << [6, 8, 0]
b << [7, 9, 0]
}
}

For normal Spock specs there would be no extra log output. For Geb specs you would see something like:

Screenshot saved as target\geb-reports\de\scrum_master\testing\extension\TestFailureGebReportingTest\70413528792100.png

When running Geb specs using a driver not implementing TakesScreenshot (e.g. HtmlUnitDriver), you would rather see:

Driver org.openqa.selenium.htmlunit.HtmlUnitDriver@4893b344 is incapable of taking screenshots

--
Alexander Kriegisch
https://scrum-master.de



Alexander Kriegisch schrieb am 05.01.2021 09:28 (GMT +07:00):

> I forgot to mention: A SpecInfo is just meta information, the best you
> can get from there is the spec class in order wo rewrite the 'if' as
>
> if (GebSpec.isAssignableFrom(iteration.feature.spec.reflection))
>
> But then you are at a dead end if you need the actual specification
> instance because you cannot get it, so there is no way to access its
> fields or methods like 'getDriver()' from there either. This is why
> Marcin's approach to use an IMethodInterceptor is exactly right. From
> the 'IMethodInvocation invocation' parameter you can easily get the
> instance, while from an 'IterationInfo iteration' you cannot. So you
> see, my quick & dirty hack without running the code is fundamentally
> wrong on several levels.
>
>

Marcin Erdmann

unread,
Jan 5, 2021, 5:54:28 AM1/5/21
to geb-...@googlegroups.com
I have to admit that I sometimes wonder if you ever do any work and not just answer people's questions online when I see answers this detailed from you, Alexander. :)

GebUser

unread,
Jan 5, 2021, 3:41:49 PM1/5/21
to Geb User Mailing List
wow!!!, thank you both! I will try those. Another question I am curious about is that "is it possible to get the shared fields from a spec using the same approach?"
I have a spec that extends a BaseSpec which has a @Share field named  testFailed 

BaseSpec extends GebReportingSpec
{

@Shared boolean  testFailed = false

}

MySpec extends BaseSpec
{
if (someErrorHappens){
testFailed=true

Marcin Erdmann

unread,
Jan 5, 2021, 4:09:26 PM1/5/21
to geb-...@googlegroups.com
Did you try calling testFailed on the spec instance obtained from IMethodInvocation.getInstance()? I would expect that to just work and if it doesn't then there's also IMethodInvocation.getSharedInstance().

Alexander Kriegisch

unread,
Jan 5, 2021, 7:48:23 PM1/5/21
to geb-...@googlegroups.com

Just a remark because Marcin already answered the actual question: We are talking Spock now, this question is no longer connected to Geb. I do not mind, but I am not so sure about other readers here.


--
Alexander Kriegisch
https://scrum-master.de


Reply all
Reply to author
Forward
0 new messages