Completed
Push — master ( 80127d...61ac21 )
by Sergii
04:02
created

RawPageContext::getBodyElement()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 1
Metric Value
dl 0
loc 4
c 3
b 1
f 1
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
/**
3
 * @author Sergii Bondarenko, <[email protected]>
4
 */
5
namespace Drupal\TqExtension\Context;
6
7
// Contexts.
8
use Drupal\DrupalExtension\Context\RawDrupalContext;
9
10
// Exceptions.
11
use WebDriver\Exception\NoSuchElement;
12
13
// Helpers.
14
use Behat\Mink\Element\NodeElement;
15
16
class RawPageContext extends RawDrupalContext
17
{
18
    /**
19
     * @var NodeElement
20
     */
21
    private static $workingElement;
22
23
    /**
24
     * @return NodeElement
25
     */
26
    public function getWorkingElement()
27
    {
28
        if (null === self::$workingElement) {
29
            $this->setWorkingElement($this->getBodyElement());
0 ignored issues
show
Bug introduced by
It seems like $this->getBodyElement() can be null; however, setWorkingElement() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
30
        }
31
32
        return self::$workingElement;
33
    }
34
35
    /**
36
     * @param NodeElement $element
37
     */
38
    public function setWorkingElement(NodeElement $element)
39
    {
40
        self::$workingElement = $element;
41
    }
42
43
    public function unsetWorkingElement()
44
    {
45
        self::$workingElement = null;
46
    }
47
48
    /**
49
     * @param string $selector
50
     *
51
     * @return NodeElement|null
52
     */
53
    public function findByCss($selector)
54
    {
55
        return $this->getWorkingElement()
56
            ->find(empty($this->getDrupalParameter('region_map')[$selector]) ? 'css' : 'region', $selector);
57
    }
58
59
    /**
60
     * @param string $selector
61
     *
62
     * @return NodeElement|null
63
     */
64
    public function findField($selector)
65
    {
66
        $selector = ltrim($selector, '#');
67
        $element = $this->getWorkingElement();
68
69
        foreach ($this->findLabels($selector) as $forAttribute => $label) {
70
            // We trying to find an ID with "-upload" suffix, because some
71
            // image inputs in Drupal are suffixed by it.
72
            foreach ([$forAttribute, "$forAttribute-upload"] as $elementID) {
73
                $field = $element->findById($elementID);
74
75
                if (null !== $field) {
76
                    return $field;
77
                }
78
            }
79
        }
80
81
        return $element->findField($selector);
82
    }
83
84
    /**
85
     * @param string $selector
86
     *
87
     * @return NodeElement
88
     */
89
    public function findButton($selector)
90
    {
91
        $button = $this->getWorkingElement()->findButton($selector);
92
93
        if (null === $button) {
94
            // @todo Improve button selector.
95
            return $this->findByInaccurateText('(//button | //input)', $selector);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->findByInaccurateT... //input)', $selector); of type Behat\Mink\Element\NodeE...ment\NodeElement[]|null adds the type Behat\Mink\Element\NodeElement[] to the return on line 95 which is incompatible with the return type documented by Drupal\TqExtension\Conte...PageContext::findButton of type Behat\Mink\Element\NodeElement|null.
Loading history...
96
        }
97
98
        return $button;
99
    }
100
101
    /**
102
     * @param string $text
103
     *
104
     * @return NodeElement|null
105
     */
106
    public function findByText($text)
107
    {
108
        return $this->findByInaccurateText('//*', $text);
109
    }
110
111
    /**
112
     * @param string $locator
113
     *   Element locator. Can be inaccurate text, inaccurate field label, CSS selector or region name.
114
     *
115
     * @throws NoSuchElement
116
     *
117
     * @return NodeElement
118
     */
119
    public function findElement($locator)
120
    {
121
        return $this->findByCss($locator)
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->findByCss($locato...->findByText($locator); of type Behat\Mink\Element\NodeE...ment\NodeElement[]|null adds the type Behat\Mink\Element\NodeElement[] to the return on line 121 which is incompatible with the return type documented by Drupal\TqExtension\Conte...ageContext::findElement of type Behat\Mink\Element\NodeElement|null.
Loading history...
122
            ?: $this->findField($locator)
123
                ?: $this->findButton($locator)
124
                    ?: $this->findByText($locator);
125
    }
126
127
    /**
128
     * Find all field labels by text.
129
     *
130
     * @param string $text
131
     *   Label text.
132
     *
133
     * @return NodeElement[]
134
     */
135
    public function findLabels($text)
136
    {
137
        $labels = [];
138
139
        foreach ($this->findByInaccurateText('//label[@for]', $text, true) as $label) {
0 ignored issues
show
Bug introduced by
The expression $this->findByInaccurateT...el[@for]', $text, true) of type object<Behat\Mink\Elemen...ment\NodeElement>>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
140
            $labels[$label->getAttribute('for')] = $label;
141
        }
142
143
        return $labels;
144
    }
145
146
    /**
147
     * @return NodeElement
148
     */
149
    public function getBodyElement()
150
    {
151
        return $this->getSession()->getPage()->find('css', 'body');
152
    }
153
154
    /**
155
     * @param NodeElement $element
156
     * @param string $attribute
157
     * @param string $value
158
     *
159
     * @return NodeElement|null
160
     */
161
    public function getParentWithAttribute($element, $attribute, $value = '')
162
    {
163
        $attribute = empty($value) ? "@$attribute" : "contains(@$attribute, '$value')";
164
165
        return $element->find('xpath', "/ancestor::*[$attribute]");
166
    }
167
168
    /**
169
     * @param string $element
170
     *   HTML element name.
171
     * @param string $text
172
     *   Element text.
173
     * @param bool $all
174
     *   Find all or only first.
175
     *
176
     * @return NodeElement|NodeElement[]|null
177
     */
178
    private function findByInaccurateText($element, $text, $all = false)
179
    {
180
        return $this->getWorkingElement()->{'find' . ($all ? 'All' : '')}(
181
            'xpath',
182
            "{$element}[text()[starts-with(., '$text')]]"
183
        );
184
    }
185
186
    /**
187
     * @param string $selector
188
     *   Element selector.
189
     * @param mixed $element
190
     *   Existing element or null.
191
     *
192
     * @throws NoSuchElement
193
     */
194
    public function throwNoSuchElementException($selector, $element)
195
    {
196
        if (null === $element) {
197
            throw new NoSuchElement(sprintf('Cannot find an element by "%s" selector.', $selector));
198
        }
199
    }
200
201
    /**
202
     * @param string $locator
203
     * @param string $selector
204
     *
205
     * @throws \RuntimeException
206
     * @throws NoSuchElement
207
     *
208
     * @return NodeElement
209
     */
210
    public function element($locator, $selector)
211
    {
212
        $map = [
213
            'button' => 'Button',
214
            'field' => 'Field',
215
            'text' => 'ByText',
216
            'css' => 'ByCss',
217
            '*' => 'Element',
218
        ];
219
220
        if (!isset($map[$locator])) {
221
            throw new \RuntimeException(sprintf('Locator "%s" was not specified.'));
222
        }
223
224
        $element = $this->{'find' . $map[$locator]}($selector);
225
        $this->throwNoSuchElementException($selector, $element);
226
227
        return $element;
228
    }
229
}
230