Pruebas Parte 2: Pruebas funcionales
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeBienvenido de nuevo a la 2ª parte del día 29. Hoy me he saltado las normas y lo he convertido en un artículo doble. Hemos hablado de las pruebas de los componentes Twig y Live... pero también tenemos que hablar de las pruebas funcionales -o de extremo a extremo- en general. Es cuando controlamos mediante programación un navegador, hacemos que haga clic en enlaces, rellene formularios, etc.
Dos cosas sobre esto. Primero, vamos a crear un sistema que me gusta mucho. Y segundo, el camino para conseguirlo va a ser... sinceramente, un poco accidentado. No es un proceso suave y eso es algo en lo que debemos trabajar como comunidad.
zenstruck/navegador
Symfony tiene herramientas de pruebas funcionales integradas, pero a mí me gusta utilizar otra biblioteca. En tu terminal, instálala con:
composer require zenstruck/browser --dev
A continuación, en la carpeta tests/, crearé un nuevo directorio llamado Functional/... luego una nueva clase llamada VoyageControllerTest. Y supongo que también podría ponerla en un directorio Controller/.
Para las tripas, pegaré una prueba terminada:
| // ... lines 1 - 2 | |
| namespace App\Tests\Functional; | |
| use App\Factory\PlanetFactory; | |
| use App\Factory\VoyageFactory; | |
| use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; | |
| use Zenstruck\Browser\Test\HasBrowser; | |
| use Zenstruck\Foundry\Test\Factories; | |
| use Zenstruck\Foundry\Test\ResetDatabase; | |
| class VoyageControllerTest extends WebTestCase | |
| { | |
| use ResetDatabase; | |
| use Factories; | |
| use HasBrowser; | |
| public function testCreateVoyage() | |
| { | |
| PlanetFactory::createOne([ | |
| 'name' => 'Earth', | |
| ]); | |
| VoyageFactory::createOne(); | |
| $this->browser() | |
| ->visit('/') | |
| ->click('Voyages') | |
| ->click('New Voyage') | |
| ->fillField('Purpose', 'Test voyage') | |
| ->selectFieldOption('Planet', 'Earth') | |
| ->click('Save') | |
| ->assertElementCount('table tbody tr', 2) | |
| ->assertSee('Bon voyage') | |
| ; | |
| } | |
| } |
Vale, estamos utilizando ResetDatabase y Factories... extiende elWebTestCase normal para pruebas funcionales... y luego HasBrowser viene de la biblioteca Browser y nos da la capacidad de llamar a $this->browser() para controlar un navegador con esta API realmente suave. Esto sigue el flujo de ir a la página del viaje, hacer clic en "Nuevo viaje", rellenar el formulario, guardar y aseverar al final. La prueba comienza con un único Voyage en la base de datos, así que después de crear uno nuevo, aseveramos que hay dos en la página.
Para ejecutarlo, utiliza el mismo comando, pero apunta al directorio Functional/:
symfony php vendor/bin/simple-phpunit tests/Functional
Y... ¡realmente pasa! ¡Genial!
Probar JavaScript con Panther
Pero espera un momento. Entre bastidores, esto no está utilizando un navegador real: sólo está haciendo peticiones falsas en PHP. Eso significa que no ejecuta JavaScript. Estamos probando la experiencia que tendría un usuario si tuviera desactivado JavaScript. Eso está bien para muchas situaciones. Sin embargo, esta vez quiero probar toda la fantasía modal.
Para ejecutar la prueba utilizando un navegador real que admita JavaScript -como Chrome- cambia a $this->pantherBrowser():
| // ... lines 1 - 11 | |
| class VoyageControllerTest extends WebTestCase | |
| { | |
| // ... lines 14 - 17 | |
| public function testCreateVoyage() | |
| { | |
| // ... lines 20 - 24 | |
| $this->pantherBrowser() | |
| // ... lines 26 - 33 | |
| ; | |
| } | |
| } |
Pruébalo:
symfony php vendor/bin/simple-phpunit tests/Functional
¡Nada! Pero un bonito error: necesitamos instalar symfony/panther. ¡Hagámoslo!
composer require symfony/panther --dev
Panther es una biblioteca PHP que puede controlar mediante programación los navegadores reales de tu máquina. Para utilizarla, también necesitamos ampliar PantherTestCase:
| // ... lines 1 - 6 | |
| use Symfony\Component\Panther\PantherTestCase; | |
| // ... lines 8 - 11 | |
| class VoyageControllerTest extends PantherTestCase | |
| { | |
| // ... lines 14 - 35 | |
| } |
Inténtalo de nuevo:
symfony php vendor/bin/simple-phpunit tests/Functional
No vemos el navegador -se abre invisiblemente en segundo plano- ¡pero ahora está utilizando Chrome! Y la prueba falla - bastante pronto:
Elemento clicable "Nuevo Viaje" no encontrado.
Hmmm. Ha hecho clic en "Viajes", pero no ha encontrado el botón "Nuevo viaje". Una característica fantástica de zenstruck/browser con Panther es que, cuando falla una prueba, hace una captura de pantalla del fallo.
Dentro del directorio var/... aquí está. Huh, la captura de pantalla muestra que seguimos en la página de inicio, como si nunca hubiéramos hecho clic en "Viajes"... aunque puedes ver que el enlace "Viajes" parece activo.
El problema es que la navegación por la página se realiza mediante Ajax... y nuestras pruebas no saben esperar a que termine. Hace clic en "Viajes"... e inmediatamente intenta hacer clic en "Nuevo viaje". Esto será lo principal que tendremos que arreglar.
Cargando un servidor de desarrollo "de prueba
Pero antes de eso, ¡veo un problema mayor! Mira los datos: ¡no proceden de nuestra base de datos de prueba! Vienen de nuestro sitio de desarrollo
Aunque no podamos verlo, Panther está controlando un navegador real. Y... un navegador real necesita acceder a nuestro sitio utilizando un servidor web real a través de una dirección web real. Como estamos utilizando el servidor web Symfony, Panther lo detectó y... ¡lo utilizó!
Pero... ¡eso no es lo que queremos! ¿Por qué? Nuestro servidor utiliza el entorno dev y la base de datos dev. Nuestras pruebas deberían utilizar el entorno test y la base de datostest.
Para solucionarlo, abre phpunit.xml.dist. Pega dos variables de entorno:
| // ... lines 1 - 3 | |
| <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
| // ... lines 5 - 9 | |
| > | |
| <php> | |
| // ... lines 12 - 17 | |
| <server name="SYMFONY_PROJECT_DEFAULT_ROUTE_URL" value="" /> | |
| <server name="PANTHER_APP_ENV" value="test" /> | |
| </php> | |
| // ... lines 21 - 40 | |
| </phpunit> |
La primera... es una especie de hack. Le dice a Panther que no utilice nuestro servidor. En su lugar, Panther iniciará ahora silenciosamente su propio servidor web utilizando el servidor web PHP incorporado. La segunda línea le dice a Panther que utilice el entorno test cuando haga eso.
En la prueba, para que sea aún más fácil ver si esto funciona, después de hacer clic en viajes, llama a ddScreenshot():
| // ... lines 1 - 11 | |
| class VoyageControllerTest extends PantherTestCase | |
| { | |
| // ... lines 14 - 17 | |
| public function testCreateVoyage() | |
| { | |
| // ... lines 20 - 24 | |
| $this->pantherBrowser() | |
| ->visit('/') | |
| ->click('Voyages') | |
| ->ddScreenshot() | |
| // ... lines 29 - 34 | |
| ; | |
| } | |
| } |
Haz una captura de pantalla, luego vuelca y muere.
Ejecuta:
symfony php vendor/bin/simple-phpunit tests/Functional
Lo hace... ¡y guarda una captura de pantalla! ¡Genial! Búscalo en var/. Y... vale. Parece que se está utilizando el nuevo servidor web... ¡pero faltan todos los estilos!
Depurar abriendo el navegador
¡Es hora de hacer un poco de trabajo detectivesco! Para entender qué está pasando, podemos decirle temporalmente a Panther que abra realmente el navegador, por ejemplo, para que podamos verlo y jugar con él.
Después de visitar, digamos ->pause():
| // ... lines 1 - 11 | |
| class VoyageControllerTest extends PantherTestCase | |
| { | |
| // ... lines 14 - 17 | |
| public function testCreateVoyage() | |
| { | |
| // ... lines 20 - 24 | |
| $this->pantherBrowser() | |
| ->visit('/') | |
| ->pause() | |
| // ... lines 28 - 35 | |
| ; | |
| } | |
| } |
Luego, para abrir el navegador, anteponemos al comando de prueba PANTHER_NO_HEADLESS=1:
PANTHER_NO_HEADLESS=1 symfony php vendor/bin/simple-phpunit tests/Functional
Y... ¡woh! Se abrió el navegador y luego se detuvo. Ahora podemos ver el código fuente de la página. Aquí está el archivo CSS. Ábrelo. Es un 404 no encontrado. ¿Por qué?
En el entorno de desarrollo, nuestros recursos se sirven a través de Symfony: no son archivos físicos reales. Si antepones a la URL index.php, funciona. Panther utiliza el servidor web PHP integrado... y necesita una regla de reescritura que le indique que envíe estas URL a través de Symfony. Sinceramente, es un detalle molesto, pero podemos arreglarlo.
De vuelta al terminal, pulsa intro para cerrar el navegador. En tests/, crea un nuevo archivo llamado router.php. Pega el código:
| // ... lines 1 - 2 | |
| if (is_file($_SERVER['DOCUMENT_ROOT'].\DIRECTORY_SEPARATOR.$_SERVER['SCRIPT_NAME'])) { | |
| return false; | |
| } | |
| $script = 'index.php'; | |
| $_SERVER = array_merge($_SERVER, $_ENV); | |
| $_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'].\DIRECTORY_SEPARATOR.$script; | |
| $_SERVER['SCRIPT_NAME'] = \DIRECTORY_SEPARATOR.$script; | |
| $_SERVER['PHP_SELF'] = \DIRECTORY_SEPARATOR.$script; | |
| require $script; |
Este es un archivo "enrutador" que utilizará el servidor web incorporado. Para decirle a Panther que lo utilice, en phpunit.xml.dist, pegaré otra variable de entorno:PANTHER_WEB_SERVER_ROUTER ajustada a ../tests/router.php:
| // ... lines 1 - 3 | |
| <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
| // ... lines 5 - 9 | |
| > | |
| <php> | |
| // ... lines 12 - 19 | |
| <server name="PANTHER_WEB_SERVER_ROUTER" value="../tests/router.php" /> | |
| </php> | |
| // ... lines 22 - 41 | |
| </phpunit> |
¡Pruébalo!
PANTHER_NO_HEADLESS=1 symfony php vendor/bin/simple-phpunit tests/Functional
Y ahora... ¡funciona! Pulsa intro para terminar. A continuación, elimina el pause().
Ejecuta de nuevo la prueba, pero sin la variable de entorno:
symfony php vendor/bin/simple-phpunit tests/Functional
Esperando la carga de la página turbo
Genial: ha llegado a nuestra línea de captura de pantalla. Ábrela. Vale, volvemos al problema original: no espera a que se cargue la página después de que hagamos clic en el enlace.
Resolver esto... no es tan sencillo como debería. Di $browser =, ciérralo e inicia una nueva cadena con $browser debajo. Entre medias, pegaré dos líneas. Ésta es de nivel inferior, pero espera a que se añada el atributo aria-busy al elementohtml, cosa que hace Turbo cuando se está cargando. Luego espera a que desaparezca:
| // ... lines 1 - 11 | |
| class VoyageControllerTest extends PantherTestCase | |
| { | |
| // ... lines 14 - 17 | |
| public function testCreateVoyage() | |
| { | |
| // ... lines 20 - 24 | |
| $browser = $this->pantherBrowser() | |
| ->visit('/') | |
| ->click('Voyages') | |
| ; | |
| $browser->client()->waitFor('html[aria-busy="true"]'); | |
| $browser->client()->waitFor('html:not([aria-busy])'); | |
| $browser | |
| ->ddScreenshot() | |
| ->click('New Voyage') | |
| ->fillField('Purpose', 'Test voyage') | |
| ->selectFieldOption('Planet', 'Earth') | |
| ->click('Save') | |
| ->assertElementCount('table tbody tr', 2) | |
| ->assertSee('Bon voyage') | |
| ; | |
| } | |
| } |
Prueba ahora:
symfony php vendor/bin/simple-phpunit tests/Functional
Luego... abre la captura de pantalla. ¡Woh! Ahora está esperando a que termine la llamada Ajax. Pero recuerda: también estamos utilizando transiciones de vista. La página se ha cargado... pero aún está en medio de la transición. Lo arreglaremos en un minuto.
Navegador personalizado y clase de prueba base
Pero antes, tenemos que limpiar esto: es demasiado trabajo. Lo que me gustaría es un nuevo método en el propio navegador, como waitForPageLoad(). ¡Y podemos hacerlo con una clase de navegador personalizada!
En el directorio tests/, crea una nueva clase llamada AppBrowser. Voy a pegar las tripas:
| // ... lines 1 - 2 | |
| namespace App\Tests; | |
| use Zenstruck\Browser\PantherBrowser; | |
| class AppBrowser extends PantherBrowser | |
| { | |
| public function waitForPageLoad(): self | |
| { | |
| $this->client()->waitFor('html[aria-busy="true"]'); | |
| $this->client()->waitFor('html:not([aria-busy])'); | |
| return $this; | |
| } | |
| } |
Esto extiende la clase normal PantherBrowser y añade un nuevo método que esas mismas dos líneas.
Cuando llamemos a $this->pantherBrowser(), ahora queremos que devuelva nuestroAppBrowser en lugar del PantherBrowser normal. Para ello, lo has adivinado, es una variable de entorno: PANTHER_BROWSER_CLASS ajustada a App\Tests\AppBrowser:
| // ... lines 1 - 3 | |
| <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
| // ... lines 5 - 9 | |
| > | |
| <php> | |
| // ... lines 12 - 20 | |
| <server name="PANTHER_BROWSER_CLASS" value="App\Tests\AppBrowser" /> | |
| </php> | |
| // ... lines 23 - 42 | |
| </phpunit> |
Para asegurarnos de que esto funciona, dd(get_class($browser));:
| // ... lines 1 - 11 | |
| class VoyageControllerTest extends PantherTestCase | |
| { | |
| // ... lines 14 - 17 | |
| public function testCreateVoyage() | |
| { | |
| // ... lines 20 - 24 | |
| $browser = $this->pantherBrowser() | |
| ->visit('/') | |
| ->click('Voyages') | |
| ; | |
| dd(get_class($browser)); | |
| // ... lines 30 - 40 | |
| } | |
| } |
Ejecuta la prueba:
symfony php vendor/bin/simple-phpunit tests/Functional
Y... ¡sí! ¡Obtenemos AppBrowser! Por desgracia, aunque el nuevo método funcionaría, no obtenemos autocompletado. Nuestro editor no tiene ni idea de que hemos intercambiado una subclase.
Para mejorar esto, hagamos una última cosa: en tests/, crea una nueva clase base de prueba: AppPantherTestCase. Pegaré el contenido:
| // ... lines 1 - 2 | |
| namespace App\Tests; | |
| use Symfony\Component\Panther\PantherTestCase; | |
| use Zenstruck\Browser\Test\HasBrowser; | |
| class AppPantherTestCase extends PantherTestCase | |
| { | |
| use HasBrowser { | |
| pantherBrowser as parentPantherBrowser; | |
| } | |
| protected function pantherBrowser(array $options = [], array $kernelOptions = [], array $managerOptions = []): AppBrowser | |
| { | |
| return $this->parentPantherBrowser($options, $kernelOptions, $managerOptions); | |
| } | |
| } |
Extiende la clase normal PantherTestCase... luego anula el método pantherBrowser(), llama al padre, pero cambia el tipo de retorno para que sea nuestro AppBrowser.
En VoyageControllerTest, cambia esto a extend AppPantherTestCase , y asegúrate de eliminar use HasBrowser:
| // ... lines 1 - 6 | |
| use App\Tests\AppPantherTestCase; | |
| // ... lines 8 - 10 | |
| class VoyageControllerTest extends AppPantherTestCase | |
| { | |
| use ResetDatabase; | |
| use Factories; | |
| // ... lines 16 - 35 | |
| } |
Luego podemos ajustar las cosas: vuelve a conectar todos estos puntos... y utiliza el nuevo método: ->waitForPageLoad()... ¡con autocompletar! Elimina elddScreenshot():
| // ... lines 1 - 10 | |
| class VoyageControllerTest extends AppPantherTestCase | |
| { | |
| // ... lines 13 - 15 | |
| public function testCreateVoyage() | |
| { | |
| // ... lines 18 - 22 | |
| $this->pantherBrowser() | |
| ->visit('/') | |
| ->click('Voyages') | |
| ->waitForPageLoad() | |
| ->click('New Voyage') | |
| ->fillField('Purpose', 'Test voyage') | |
| ->selectFieldOption('Planet', 'Earth') | |
| ->click('Save') | |
| ->assertElementCount('table tbody tr', 2) | |
| ->assertSee('Bon voyage') | |
| ; | |
| } | |
| } |
¡Y veamos dónde estamos!
symfony php vendor/bin/simple-phpunit tests/Functional
¡Más lejos!
Campo del formulario "Propósito" no encontrado.
Así que hizo clic en Viajes, hizo clic en "Nuevo viaje"... pero no encontró el campo de formulario. Si miramos hacia abajo en la captura de pantalla del error, podemos ver por qué: ¡el contenido del modal todavía se está cargando! Puede que veas el formulario en la captura de pantalla -a veces la captura de pantalla se produce un momento después, por lo que el formulario es visible-, pero éste es el problema.
Desactivar las transiciones de vista
Ah, pero antes de arreglar esto, también quiero desactivar las transiciones de vista. Entemplates/base.html.twig, la forma más fácil de asegurarse de que las transiciones de vista no estropean nuestras pruebas es eliminarlas. Digamos que si app.environment != 'test, entonces renderiza esta etiqueta meta:
| <html> | |
| <head> | |
| // ... lines 4 - 6 | |
| {% if app.environment != 'test' %} | |
| <meta name="view-transition"> | |
| {% endif %} | |
| // ... lines 10 - 16 | |
| </head> | |
| // ... lines 18 - 97 | |
| </html> |
Esperando a que se cargue el modal
En fin, volvamos a nuestro fallo. Cuando hacemos clic para abrir el modal, lo que necesitamos es esperar a que se abra el modal -que en realidad es instantáneo-, pero también esperar a que termine de cargarse el <turbo-frame>que hay dentro.
Abre AppBrowser. Voy a pegar dos métodos más:
| // ... lines 1 - 4 | |
| use Facebook\WebDriver\WebDriverBy; | |
| // ... lines 6 - 7 | |
| class AppBrowser extends PantherBrowser | |
| { | |
| // ... lines 10 - 17 | |
| public function waitForDialog(): self | |
| { | |
| $this->client()->wait()->until(function() { | |
| return $this->crawler()->filter('dialog[open]')->count() > 0; | |
| }); | |
| if ($this->crawler()->filter('dialog[open] turbo-frame')->count() > 0) { | |
| $this->waitForTurboFrameLoad(); | |
| } | |
| return $this; | |
| } | |
| public function waitForTurboFrameLoad(): self | |
| { | |
| $this->client()->wait()->until(function() { | |
| return $this->crawler()->filter('turbo-frame[aria-busy="true"]')->count() === 0; | |
| }); | |
| return $this; | |
| } | |
| } |
El primero - waitForDialog() - espera hasta que ve un diálogo en la página con un atributo open. Y, si ese dialog abierto tiene un <turbo-frame>, espera a que se cargue: espera hasta que no haya ningún marco aria-busy en la página.
En VoyageControllerTest, después de hacer clic en "Nuevo viaje", digamos ->waitForDialog():
| // ... lines 1 - 10 | |
| class VoyageControllerTest extends AppPantherTestCase | |
| { | |
| // ... lines 13 - 15 | |
| public function testCreateVoyage() | |
| { | |
| // ... lines 18 - 22 | |
| $this->pantherBrowser() | |
| // ... lines 24 - 26 | |
| ->click('New Voyage') | |
| ->waitForDialog() | |
| ->fillField('Purpose', 'Test voyage') | |
| // ... lines 30 - 33 | |
| ; | |
| } | |
| } |
Y ahora
symfony php vendor/bin/simple-phpunit tests/Functional
¡Tan cerca!
tabletbodytresperaba 2 elementos en la página, pero sólo encontró 1.
¡Eso viene de aquí abajo! ¿Cuál es el problema esta vez? ¡Volvamos a la captura de pantalla de error! Ah: hemos rellenado el formulario, parece que incluso hemos pulsado Guardar... ¡pero estamos afirmando demasiado rápido!
Recuerda: esto se envía a un <turbo-frame>, así que tenemos que esperar a que ese marco termine de cargarse. Y tenemos una forma de hacerlo:->waitForTurboFrameLoad(). También añadiré una línea para afirmar que no podemos ver ningún diálogo abierto: para comprobar que el modal se cerró:
| // ... lines 1 - 10 | |
| class VoyageControllerTest extends AppPantherTestCase | |
| { | |
| // ... lines 13 - 15 | |
| public function testCreateVoyage() | |
| { | |
| // ... lines 18 - 22 | |
| $this->pantherBrowser() | |
| ->visit('/') | |
| ->click('Voyages') | |
| ->waitForPageLoad() | |
| ->click('New Voyage') | |
| ->waitForDialog() | |
| ->fillField('Purpose', 'Test voyage') | |
| ->selectFieldOption('Planet', 'Earth') | |
| ->click('Save') | |
| ->waitForTurboFrameLoad() | |
| ->assertElementCount('table tbody tr', 2) | |
| ->assertNotSeeElement('dialog[open]') | |
| ->assertSee('Bon voyage') | |
| ; | |
| } | |
| } |
Ejecuta la prueba una vez más:
symfony php vendor/bin/simple-phpunit tests/Functional
Pasa. ¡Guau! Lo admito, ha sido trabajo, ¡demasiado trabajo! Pero me encanta el resultado final.
Mañana, en nuestro último día, hablaremos del rendimiento. Y a diferencia de hoy, las cosas se pondrán rápidamente en su sitio, te lo prometo.
15 Comments
Right after
composer require symfony/panther --devI had to runcomposer require --dev dbrekelmans/bdi && vendor/bin/bdi detect driversHey @apphancer ,
Thanks for sharing this with others! It might be helpful :)
Cheers!
If you enable
autocomplete=truefor theplanetfield in theVoyageTypeform (It's works great with Live Components at the moment), your Panther test might not work, because for selecting planet field (selectFieldOption('Planet', 'Earth')), crawler detects htmlinputfield instead ofselectfield, and throws an exception.To fix this, set
autocomplete=falsefor testing:Hey @Olexandr!
You're solution definitely works and sometimes, to stay sane when working with Panther, it's what you have to do. :)
I am curious: was it finding the autocomplete
inputor not finding theselect? I believe Panther cannot find things that aren't visible so I would have expected an "element not found" exception (because autocomplete hides the select and replaces with an input).--Kevin
Hi @kbond!
Actually, an exception throws by crawler on
autocomplete=truesetting, looks like:Xdebug shows that crawler detects an
inputtype field, which is not whatselectFieldOptionmethod expects. So the simplest way, is to disable autocomplete and to give crawler the expected type)Exception thrown on: https://github.com/symfony/panther/blob/main/src/DomCrawler/Field/ChoiceFormField.php#L199
The state of variables in that moment:
autocomplete=falsegives:Hmm, I see... Then yeah, sounds like your solution is the easiest, reasonable fix...
I have issues with test execution speed :/
It looks like that
waitForTurboFrameLoadcheck for anyturbo-frame[aria-busy="true"]in the page is executed before Turbo adds thearia-busy="true"attribute to the frame.I had similar issue with the modal loading. The tests complain about not finding the Purpose input. I added a wait for the
form[name="voyage"]before callingfillField.Does anyone have an idea of what can be done to get proper waiting?
Hello @fbourigault
Great catch. in fact with turbo you should first wait for it to appear on the page, and then wait for disappear, it can be achieved with some
spin()functions like here described https://docs.behat.org/en/v2.5/cookbook/using_spin_functions.htmlCheers!
Hey everyone
If anyone tries to get this whole Panther system working in the context of a containerized environment (in my case: a DDEV wrapper around Docker), you will need to jump through some extra hoops. And read some stackoverflow stuff. And documentation entries. Blergh...
Anyway, here's the idea: we want to get Panther working with Chromium in a DDEV context, and if you try that just using your "standard" DDEV config, you will be greeted by the following error:
Seems obvious enough; just run that entire suggestion, right? Except
vendor/bin/bdi detect driversdoes not do anything. That is because our web container is lacking Chromium. Let's fix that:Awesome, lots of installation text. Run
vendor/bin/bdi detect driversagain, and hooray:Try to run the tests again:
php ./vendor/bin/simple-phpunit. I do so from within the DDEV web container as I am already there, but you could of course also run it from outside. But, alas:If you Google that error a bit, you will find a whole bunch of expert tech talk on configuring Selenium in the context of Python or Java or whatever, all of which is more or less impossible for me to understand properly. But apparently it might be solved by starting the chromium instance with a
--no-sandboxargument. And luckily, the Panther docs cover that topic specifically here: https://symfony.com/doc/current/testing/end_to_end.html#chrome-specific-environment-variables. UsePANTHER_NO_SANDBOXto "Disable Chrome's sandboxing (unsafe, but allows to use Panther in containers)" - that sounds like it is exactly what we need. Add this to your.env.test.local:If you now run the tests again, things will appear to work. Hooray, right? Except the browser that is running here is not finding your assets. It is explained in the main script how you need to use the
tests/router.phpto get this to work, but you also need to start the Chromium browser with the extra environment argument--disable-dev-shm-usage. This can be added to.env.test.localas well:Note: these
.envthings can of course also be added tophpunit.xml.distas<server>tags under<php>, but since we are assuming DDEV to be a purely local develoment environment, we do not want to activate all this stuff in a version controlled file.TLDR: let us automate a great deal of this process in DDEV by working with a custom Dockerfile and a custom phpunit DDEV command.
File:
.ddev/web-build/Dockerfile.chromium:File:
.ddev/commands/web/phpunitYou may also want to add
/driversto your local.gitignore, as this is probably only useful for local development. No need to pester your non-containered colleagues with it.Do not forget our
.env.test.localfile:And finally, remember to have
dbrekelmans/bdias a composer dev package throughcomposer require dbrekelmans/bdi --dev.With these things in place, all you need to do to get started on an otherwise standard DDEV config is:
Your initial DDEV start-up will take some time due to Chromium installation, but I haven't found a way around using the full Chrome and that takes a while to install... Maybe some other
glorious fooltech hero can figure that out.Yo @TomDeRoo!
This is a crazy amount of work I'd love for us to make it LESS work but for now, your comment can at least be here to help.
Thanks!
Hey @TomDeRoo
Nicely done, thank you for sharing your solution with others!
Cheers!
I was struggling a bit with issues with the
->click('New Voyage')resulting in the dialog not opening.Eventually, it turned out that the first line
$this->client()->waitFor('html[aria-busy="true"]');in thewaitForPageLoad()method was preventing this from working. Any ideas why?Anyway, I've commented it out and the dialog is displaying and tests are all green again.
Hey @apphancer
That's unexpected. I believe your site is running so fast that Turbo finishes loading before your
waitFor()call. IIRC the second argument of that method is the amount of milliseconds it should wait before it "gives up", you could set it to 1 second so it doesn't wait for too long.Let me know if it worked or if I'm completely wrong :P
Cheers!
Thanks for the suggestion. I gave it a shot but no luck. Looking at the images in the error-screenshot folder, it looks like the /voyage page loads but no dialog opens.
This is the actual error:
There was 1 error:
1) App\Tests\Functional\VoyageControllerTest::testCreateVoyage
Facebook\WebDriver\Exception\TimeoutException:
/Users/m/Desktop/code-last-stack/vendor/php-webdriver/webdriver/lib/WebDriverWait.php:71
I don't really get it why it's saying that it's timing out because of that line, even when I've reduced the $timeoutInSecond parameter to 1 second.
It's no big deal. As I said, it's working after having commented out the first line of the waitForPageLoad() method. It's more curiosity than anything else.
Yea, that's intriguing, but sadly I don't know the answer. At least you found a solution :)
Let me know if you discover the mystery. Cheers!
"Houston: no signs of life"
Start the conversation!