Skip to content
Snippets Groups Projects
MinkTestCase.php 11.4 KiB
Newer Older
<?php

/**
 * Abstract base class for PHPUnit test cases using Mink.
 *
 * PHP version 7
 *
 * Copyright (C) Villanova University 2010.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
Demian Katz's avatar
Demian Katz committed
 * @category VuFind
 * @package  Tests
 * @author   Demian Katz <demian.katz@villanova.edu>
 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
Demian Katz's avatar
Demian Katz committed
 * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
 */
namespace VuFindTest\Unit;
use Behat\Mink\Driver\Selenium2Driver;
use Behat\Mink\Element\Element;
use Behat\Mink\Session;
use DMore\ChromeDriver\ChromeDriver;
use VuFind\Config\Locator as ConfigLocator;
use VuFind\Config\Writer as ConfigWriter;

/**
 * Abstract base class for PHPUnit test cases using Mink.
 *
Demian Katz's avatar
Demian Katz committed
 * @category VuFind
 * @package  Tests
 * @author   Demian Katz <demian.katz@villanova.edu>
 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
Demian Katz's avatar
Demian Katz committed
 * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
Chris Hallberg's avatar
Chris Hallberg committed
abstract class MinkTestCase extends DbTestCase
     * Modified configurations
Demian Katz's avatar
Demian Katz committed
    protected $modifiedConfigs = [];
Demian Katz's avatar
Demian Katz committed
    /**
Demian Katz's avatar
Demian Katz committed
     *
Demian Katz's avatar
Demian Katz committed
     */
Demian Katz's avatar
Demian Katz committed

    /**
     * Reconfigure VuFind for the current test.
     *
     * @param array $configs Array of settings to change. Top-level keys correspond
     * with config filenames (i.e. use 'config' for config.ini, etc.); within each
     * file's array, top-level key is config section. Within each section's array
     * are key-value configuration pairs.
     * @param array $replace Array of config files to completely override (as
     * opposed to modifying); if a config file from $configs is included in this
     * array, the $configs setting will be used as the entire configuration, and
     * the defaults from the config/vufind directory will be ignored.
Demian Katz's avatar
Demian Katz committed
     *
     * @return void
     */
    protected function changeConfigs($configs, $replace = [])
Demian Katz's avatar
Demian Katz committed
    {
        foreach ($configs as $file => $settings) {
            $this->changeConfigFile($file, $settings, in_array($file, $replace));
Demian Katz's avatar
Demian Katz committed
        }
    }

    /**
     * Support method for changeConfig; act on a single file.
     *
     * @param string $configName Configuration to modify.
     * @param array  $settings   Settings to change.
     * @param bool   $replace    Should we replace the existing config entirely
     * (as opposed to extending it with new settings)?
Demian Katz's avatar
Demian Katz committed
     *
     * @return void
     */
    protected function changeConfigFile($configName, $settings, $replace = false)
Demian Katz's avatar
Demian Katz committed
    {
        $file = $configName . '.ini';
        $local = ConfigLocator::getLocalConfigPath($file, null, true);
        if (!in_array($configName, $this->modifiedConfigs)) {
            if (file_exists($local)) {
                // File exists? Make a backup!
                copy($local, $local . '.bak');
            } else {
                // File doesn't exist? Make a baseline version.
                copy(ConfigLocator::getBaseConfigPath($file), $local);
            }
Demian Katz's avatar
Demian Katz committed

        // If we're replacing the existing file, wipe it out now:
        if ($replace) {
            file_put_contents($local, '');
        }

Demian Katz's avatar
Demian Katz committed
        $writer = new ConfigWriter($local);
        foreach ($settings as $section => $contents) {
            foreach ($contents as $key => $value) {
                $writer->set($section, $key, $value);
            }
        }
        $writer->save();
    }

    /**
     * Sleep if necessary.
     *
     * @param int $secs Seconds to sleep
     *
     * @return void
     */
    protected function snooze($secs = 1)
    {
        $snoozeMultiplier = floatval(getenv('VUFIND_SNOOZE_MULTIPLIER'));
        if ($snoozeMultiplier <= 0) {
            $snoozeMultiplier = 1;
        usleep(1000000 * $secs * $snoozeMultiplier);
    }

    /**
     * Test an element for visibility.
     *
     * @param Element $element Element to test
     *
     * @return bool
     */
    protected function checkVisibility(Element $element)
    {
        return $element->isVisible();
    /**
     * Get the Mink driver, initializing it if necessary.
     *
     * @return Selenium2Driver
     */
    protected function getMinkDriver()
    {
        $driver = getenv('VUFIND_MINK_DRIVER') ?? 'selenium';
        if ($driver === 'chrome') {
            return new ChromeDriver('http://localhost:9222', null, 'data:;');
        }
        $browser = getenv('VUFIND_SELENIUM_BROWSER') ?? 'firefox';
        return new Selenium2Driver($browser);
    }

    /**
     * Get a Mink session.
     *
     * @return Session
     */
    protected function getMinkSession()
    {
        if (empty($this->session)) {
            $this->session = new Session($this->getMinkDriver());
            $this->session->start();
        }
        return $this->session;
    }

    /**
     * Shut down the Mink session.
     *
     * @return void
     */
    protected function stopMinkSession()
    {
        if (!empty($this->session)) {
            $this->session->stop();
            $this->session = null;
        }
    }

    /**
     * Get base URL of running VuFind instance.
     *
     * @param string $path Relative path to add to base URL.
     *
     * @return string
     */
    protected function getVuFindUrl($path = '')
    {
        $base = getenv('VUFIND_URL');
        if (empty($base)) {
            $base = 'http://localhost/vufind';
        }
        return $base . $path;
    }

Demian Katz's avatar
Demian Katz committed
    /**
     * Restore configurations to the state they were in prior to a call to
     * changeConfig().
     *
     * @return void
     */
    protected function restoreConfigs()
    {
        foreach ($this->modifiedConfigs as $current) {
            $file = $current . '.ini';
            $local = ConfigLocator::getLocalConfigPath($file, null, true);
            $backup = $local . '.bak';

            // Do we have a backup? If so, restore from it; otherwise, just
            // delete the local file, as it did not previously exist:
            unlink($local);
            if (file_exists($backup)) {
                rename($backup, $local);
            }
        }
Demian Katz's avatar
Demian Katz committed
    }

    /**
     * Wait for an element to exist, then retrieve it.
     *
     * @param Element $page     Page element
     * @param string  $selector CSS selector
     * @param int     $timeout  Wait timeout (in ms)
     *
     * @return mixed
     */
    protected function findCss(Element $page, $selector, $timeout = 1000)
    {
        $session = $this->getMinkSession();
        $session->wait($timeout, "$('$selector').length > 0");
        $result = $page->find('css', $selector);
        $this->assertTrue(is_object($result), "Selector not found: $selector");
    /**
     * Click on a CSS element.
     *
     * @param Element $page     Page element
     * @param string  $selector CSS selector
     * @param int     $timeout  Wait timeout (in ms)
     *
     * @return mixed
     */
    protected function clickCss(Element $page, $selector, $timeout = 1000)
    {
        $result = $this->findCss($page, $selector, $timeout);
        for ($tries = 0; $tries < 3; $tries++) {
            try {
                $result->click();
                return $result;
            } catch (\Exception $e) {
                // Expected click didn't work... snooze and retry
                $this->snooze();
            }
        }
        throw $e ?? new \Exception('Unexpected state reached.');
    }

Demian Katz's avatar
Demian Katz committed
    /**
     * Set a value within an element selected via CSS; retry if set fails
     * due to browser bugs.
     *
     * @param Element $page     Page element
     * @param string  $selector CSS selector
     * @param string  $value    Value to set
     * @param int     $timeout  Wait timeout for CSS selection (in ms)
     * @param int     $retries  Retry count for set loop
     *
     * @return mixed
     */
    protected function findCssAndSetValue(Element $page, $selector, $value,
        $timeout = 1000, $retries = 6
    ) {
        $field = $this->findCss($page, $selector, $timeout);

        // Workaround for Chromedriver bug; sometimes setting a value
        // doesn't work on the first try.
        for ($i = 0; $i < $retries; $i++) {
            $field->setValue($value);
            // Did it work? If so, we're done and can leave....
            if ($field->getValue() === $value) {
                return;
            }
        }

        throw new \Exception('Failed to set value after ' . $retries . ' attempts.');
    }

    /**
     * Retrieve a link and assert that it exists before returning it.
     *
     * @param Element $page Page element
     * @param string  $text Link text to match
     *
     * @return mixed
     */
    protected function findAndAssertLink(Element $page, $text)
    {
Demian Katz's avatar
Demian Katz committed
        $link = $page->findLink($text);
        $this->assertTrue(is_object($link));
        return $link;
Demian Katz's avatar
Demian Katz committed
    /**
     * Check whether an element containing the specified text exists.
     *
     * @param Element $page     Page element
     * @param string  $selector CSS selector
     * @param string  $text     Expected text
     *
     * @return bool
     */
    protected function hasElementsMatchingText(Element $page, $selector, $text)
    {
        foreach ($page->findAll('css', $selector) as $current) {
            if ($text === $current->getText()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Search for the specified query.
     *
Demian Katz's avatar
Demian Katz committed
     * @param string $query   Search term(s)
     * @param string $handler Search type (optional)
     *
     * @return \Behat\Mink\Element\Element
     */
Demian Katz's avatar
Demian Katz committed
    protected function performSearch($query, $handler = null)
    {
        $session = $this->getMinkSession();
        $session->visit($this->getVuFindUrl() . '/Search/Home');
        $page = $session->getPage();
        $this->findCss($page, '#searchForm_lookfor')->setValue($query);
Demian Katz's avatar
Demian Katz committed
        if ($handler) {
            $this->findCss($page, '#searchForm_type')->setValue($handler);
        }
        $this->findCss($page, '.btn.btn-primary')->click();
        $this->snooze();
        return $page;
    }

    /**
     * Standard setup method.
     *
     * @return void
     */
    public function setUp()
    {
        // Give up if we're not running in CI:
        if (!$this->continuousIntegrationRunning()) {
            return $this->markTestSkipped('Continuous integration not running.');
        }
Demian Katz's avatar
Demian Katz committed

        // Reset the modified configs list.
        $this->modifiedConfigs = [];
Demian Katz's avatar
Demian Katz committed
    }

    /**
     * Standard teardown method.
     *
     * @return void
     */
    public function tearDown()
    {
        $this->stopMinkSession();
Demian Katz's avatar
Demian Katz committed
        $this->restoreConfigs();
    }

    /**
     * Standard tear-down.
     *
     * @return void
     */
    public static function tearDownAfterClass()
    {
        // No teardown actions at this time.