sensiolabs/BehatPageObjectExtension

Access to entity manager inside page object in V3

Closed this issue · 15 comments

Hi,

Using behat V3, i'm trying to do what I used to do in V2 with page object: convert a name into an ID in order to access the url.

Simple example:

class AdminListFaq extends Page
{
    /**
     * @var string $path
     */
    protected $path = '/{id}/faq/list';

    /**
     * {@inheritdoc}
     */
    protected function getPath()
    {
       $site = $this->entityManager->getRepository('Model:Site')->findOneByHost($this->parameter);

        return strtr($this->path, ['{id}' => $site->getId()]);
    }

And in Page

/**
 * Page
 */
class Page extends BasePage
{
    /**
     * The symfony kernel
     *
     * @var KernelInterface $kernel
     */
    protected $kernel;

    /**
     * The symfony container
     * @var Symfony\Component\DependencyInjection\ContainerInterface container
     */
    protected $container;

    /**
     * The doctrine entity manager
     * @var Doctrine\ORM\EntityManager
     */
    protected $entityManager;

    /**
     * The parameter
     *
     * @var mixed parameter
     */
    protected $parameter;

    /**
     * Set KernelInterface
     *
     * @param KernelInterface $kernel
     */
    public function setKernel(KernelInterface $kernel)
    {
        $this->kernel        = $kernel;
        $this->container     = $kernel->getContainer();
        $this->entityManager = $this->container->get('doctrine.orm.entity_manager');
    }

.....

It was working in V2, but not in V3 (the entityManager is null). Am I missing something please ?

PS: My Behat config is :

default:
    autoload: %paths.base%/features/Context
    extensions:
        Behat\MinkExtension:
            base_url:   http://example.dev/admin_behat.php
            files_path: apps/features/files
            default_session:    goutte
            javascript_session: selenium2
            goutte:    ~
            selenium2: ~
        Behat\Symfony2Extension:
            kernel:
                env:   behat
                debug: true
                path: admin/AdminKernel.php
                class: AdminKernel
                bootstrap: apps/autoload.php
        SensioLabs\Behat\PageObjectExtension:
            namespaces:
                page: Context\Page
                element: Context\Page\Element
        VIPSoft\DoctrineDataFixturesExtension\Extension:
            lifetime:    feature
            autoload:    false
            directories:
                - apps/features/Context/Fixtures
            fixtures:    ~
            migrations: ~
        Sanpi\Behatch\Extension: ~
    suites:
        default:
            contexts:
                - Context\FeatureContext
                - Context\UserContext
                - Context\PageContext
                - behatch:browser
                - behatch:debug

How do you make sure that setKernel is called on a page object?

I don't I thought it was some magic until I realize it didn't work. That's why i'm asking in here :-)

It also didn't work in v2. There's no magic here :)

Yet I can assure you that those piece of code was working for v2.
Anyway, do you have a lead on how to achieve this please ?

I would like to avoid put my ID harcoded in URL's mainly because
1- it's ugly
2- when reloading the DB, ID changes.

PS: With Behat v2, my FeatureContext had the kernel, and it was implementing the PageObjectAwareInterface

ZAP, I'm onto something I just saw. I'll update in here if it works.

There's no way it worked out of the box. If you had it working with Behat 2 you must have implemented it yourself (custom factory or sth). Behat 2 and 3 are only able to initialize contexts.

I have a somewhat similar need - inject some Symfony service in my pageObjects.

I have tried the simple way: using the "@service" notation in the parameters in behat.yml (in key extensions.PageObjectExtension.factory), but what I got injected was the string, not the service itself.

So I resorted to overtaking the factory itself, using a custom service, and then could inject whatever I needed.

Good but a bit clunky.

I have seen that the Symfony2 Behat extension mentions explicitly the fact that it converts "@service" tags in the definition of contexts in the appropriate services (https://github.com/Behat/Symfony2Extension/blob/master/doc/index.rst)

It would be nice if the pageObject extension did the same for the parameters passed to the factory.

@jakzal The way it worked before:

  1. In the FeatureContext implement PageObjectAwareInterface
  2. Inject the kernel and implement the getPage() method with a trick :
    /**
     * @param string $name
     *
     * @return Page
     */
    public function getPage($name)
    {
       .....

        if ($page instanceof Page) {
            $page->setKernel($this->kernel);
        }

        return $page;
    }
  1. Inside a Page class, retrieve the EntityManager
  2. In the xxxPage class, manipulate the EntityManager to retrieve objects via ID.

IMO, it should be the main feature of this bundle. We're not testing applications where URL / Pages have no parameters and writing a test like

Given I am on the "admin list faq" page with parameter "543"

is too damn ugly and not reusable.

So i'm +1 for @gggeek if the solution he proposes allows us to achieve that.
Btw, can you gist what you've done right now to get this thing working please ?

The interface suffix was removed in 2.0. The interface is called PageObjectAware now.

It would probably cleaner to implement your own page object factory to construct page objects with the kernel. I give it some thought and get back to you after SymfonyCon ;)

@tristanbes ask and you shall receive :-) https://gist.github.com/gggeek/d71b10e21e0480a562cf

Note that while I do not pass around the DIC itself (considered bad practice), I do pass an array of services, which also makes the dependency non explicit. Purists would probably insist that I pass instead the single services into the PageObject constructor...

Great thanks, that'll do by the time @jakzal consider a solution ;-)

Hi @gggeek ,
I've tried to inject the entity manager using your method but I got this exception

ParameterNotFoundException: The service "serv.behat.page_factory" has a dependency on a non-existent parameter "sensio_labs.page_object_extension.page_factory.page_parameters".
This is my service

        <service id="serv.behat.page_factory" class="Context\Page\ServiceAwareFactory">
            <argument type="service" id="mink" />
            <argument type="service" id="sensio_labs.page_object_extension.class_name_resolver" />
            <argument>%sensio_labs.page_object_extension.page_factory.page_parameters%</argument>
            <argument type="collection">
                <argument type="service" id="doctrine.orm.entity_manager" />
            </argument>
        </service>

If I remove this parameter I got this
ServiceNotFoundException: The service "serv.behat.page_factory" has a dependency on a non-existent service "mink".

This is what I got in my composer.json

        "behat/mink": "dev-master",
        "behat/mink-browserkit-driver": "dev-master",
        "behat/mink-goutte-driver": "dev-master",
        "behat/mink-selenium2-driver": "1.2.*@dev"
       "sensiolabs/behat-page-object-extension": "2.0.*@dev",

It's seems that My application do not recognize any of mink parameters.
What did I do wrong ?

Hey @jakzal Did you have some time to think about this awesome feature ? 🎯

@jakzal, did you have time to think about it ? 😃

I'm 👎 on adding this feature to the page object library as it's too framework specific.

I don't find it clean either. Using the entity manager in page objects goes against their responsibility (which is manipulating/extracting details of the page).

I would not add this feature to the library. I would, however, accept a docs PR with a cookbook how to implement it yourself.