Passed
Push — 4 ( debb3e...dcebf5 )
by Maxime
09:23 queued 01:02
created

CmsUiContext::iPressTheKeyInTheField()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Framework\Tests\Behaviour;
4
5
use Behat\Behat\Context\Context;
0 ignored issues
show
Bug introduced by
The type Behat\Behat\Context\Context was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use Behat\Behat\Hook\Scope\AfterStepScope;
0 ignored issues
show
Bug introduced by
The type Behat\Behat\Hook\Scope\AfterStepScope was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use Behat\Mink\Element\Element;
0 ignored issues
show
Bug introduced by
The type Behat\Mink\Element\Element was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use Behat\Mink\Element\NodeElement;
0 ignored issues
show
Bug introduced by
The type Behat\Mink\Element\NodeElement was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use Behat\Mink\Selector\Xpath\Escaper;
0 ignored issues
show
Bug introduced by
The type Behat\Mink\Selector\Xpath\Escaper was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
10
use Behat\Mink\Session;
0 ignored issues
show
Bug introduced by
The type Behat\Mink\Session was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
11
use SilverStripe\BehatExtension\Context\MainContextAwareTrait;
0 ignored issues
show
Bug introduced by
The type SilverStripe\BehatExtens...t\MainContextAwareTrait was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
use SilverStripe\BehatExtension\Utility\StepHelper;
0 ignored issues
show
Bug introduced by
The type SilverStripe\BehatExtension\Utility\StepHelper was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
14
/**
15
 * CmsUiContext
16
 *
17
 * Context used to define steps related to SilverStripe CMS UI like Tree or Panel.
18
 */
19
class CmsUiContext implements Context
20
{
21
    use MainContextAwareTrait;
22
    use StepHelper;
23
24
    /**
25
     * Get Mink session from MinkContext
26
     *
27
     * @param string $name
28
     * @return Session
29
     */
30
    public function getSession($name = null)
31
    {
32
        return $this->getMainContext()->getSession($name);
33
    }
34
35
    /**
36
     * Wait until CMS loading overlay isn't present.
37
     * This is an addition to the "ajax steps" logic in
38
     * SilverStripe\BehatExtension\Context\BasicContext
39
     * which also waits for any ajax requests to finish before continuing.
40
     *
41
     * The check also applies in when not in the CMS, which is a structural issue:
42
     * Every step could cause the CMS to be loaded, and we don't know if we're in the
43
     * CMS UI until we run a check.
44
     *
45
     * Excluding scenarios with @modal tag is required,
46
     * because modal dialogs stop any JS interaction
47
     *
48
     * @AfterStep
49
     * @param AfterStepScope $event
50
     */
51
    public function handleCmsLoadingAfterStep(AfterStepScope $event)
52
    {
53
        // Manually exclude @modal
54
        if ($this->stepHasTag($event, 'modal')) {
55
            return;
56
        }
57
58
        $timeoutMs = $this->getMainContext()->getAjaxTimeout();
59
        $this->getSession()->wait(
60
            $timeoutMs,
61
            "document.getElementsByClassName('cms-content-loading-overlay').length == 0"
62
        );
63
    }
64
65
    /**
66
     * @Then /^I should see the CMS$/
67
     */
68
    public function iShouldSeeTheCms()
69
    {
70
        $page = $this->getSession()->getPage();
71
        $cms_element = $page->find('css', '.cms');
72
        assertNotNull($cms_element, 'CMS not found');
73
    }
74
75
    /**
76
     * @Then /^I should see a "([^"]*)" notice$/
77
     */
78
    public function iShouldSeeANotice($notice)
79
    {
80
        $this->getMainContext()->assertElementContains('.notice-wrap', $notice);
81
    }
82
83
    /**
84
     * @Then /^I should see a "([^"]*)" message$/
85
     */
86
    public function iShouldSeeAMessage($message)
87
    {
88
        $this->getMainContext()->assertElementContains('.message', $message);
89
    }
90
91
    protected function getCmsTabsElement()
92
    {
93
        $this->getSession()->wait(
94
            5000,
95
            "window.jQuery && window.jQuery('.cms-content-header-tabs').size() > 0"
96
        );
97
98
        $page = $this->getSession()->getPage();
99
        $cms_content_header_tabs = $page->find('css', '.cms-content-header-tabs');
100
        assertNotNull($cms_content_header_tabs, 'CMS tabs not found');
101
102
        return $cms_content_header_tabs;
103
    }
104
105
    protected function getCmsContentToolbarElement()
106
    {
107
        $this->getSession()->wait(
108
            5000,
109
            "window.jQuery && window.jQuery('.cms-content-toolbar').size() > 0 "
110
            . "&& window.jQuery('.cms-content-toolbar').children().size() > 0"
111
        );
112
113
        $page = $this->getSession()->getPage();
114
        $cms_content_toolbar_element = $page->find('css', '.cms-content-toolbar');
115
        assertNotNull($cms_content_toolbar_element, 'CMS content toolbar not found');
116
117
        return $cms_content_toolbar_element;
118
    }
119
120
    protected function getCmsTreeElement()
121
    {
122
        $this->getSession()->wait(
123
            5000,
124
            "window.jQuery && window.jQuery('.cms-tree').size() > 0"
125
        );
126
127
        $page = $this->getSession()->getPage();
128
        $cms_tree_element = $page->find('css', '.cms-tree');
129
        assertNotNull($cms_tree_element, 'CMS tree not found');
130
131
        return $cms_tree_element;
132
    }
133
134
    /**
135
     * @Given /^I should see a "([^"]*)" button in CMS Content Toolbar$/
136
     */
137
    public function iShouldSeeAButtonInCmsContentToolbar($text)
138
    {
139
        $cms_content_toolbar_element = $this->getCmsContentToolbarElement();
140
141
        $element = $cms_content_toolbar_element->find('named', array('link_or_button', "'$text'"));
142
        assertNotNull($element, sprintf('%s button not found', $text));
143
    }
144
145
    /**
146
     * @When /^I should see "([^"]*)" in the tree$/
147
     */
148
    public function stepIShouldSeeInCmsTree($text)
149
    {
150
        // Wait until visible
151
        $cmsTreeElement = $this->getCmsTreeElement();
152
        $element = $cmsTreeElement->find('named', array('content', "'$text'"));
153
        assertNotNull($element, sprintf('%s not found', $text));
154
    }
155
156
    /**
157
     * @When /^I should not see "([^"]*)" in the tree$/
158
     */
159
    public function stepIShouldNotSeeInCmsTree($text)
160
    {
161
        // Wait until not visible
162
        $cmsTreeElement = $this->getCmsTreeElement();
163
        $element = $cmsTreeElement->find('named', array('content', "'$text'"));
164
        assertNull($element, sprintf('%s found', $text));
165
    }
166
167
    /**
168
     * @When /^I should (|not )see "([^"]*)" in the cms list$/
169
     */
170
    public function stepIShouldSeeInCmsList($negate, $text)
171
    {
172
        // Wait until visible
173
        $this->getSession()->wait(
174
            5000,
175
            "document.querySelector('.cms-lists') !== null"
176
        );
177
        $page = $this->getSession()->getPage();
178
        $cmsListElement = $page->find('css', '.cms-list');
179
        assertNotNull($cmsListElement, 'CMS list not found');
180
181
        // Check text within this element
182
        $element = $cmsListElement->find('named', array('content', "'$text'"));
183
        if (strstr($negate, 'not')) {
184
            assertNull($element, sprintf('Unexpected %s found in cms list', $text));
185
        } else {
186
            assertNotNull($element, sprintf('Expected %s not found in cms list', $text));
187
        }
188
    }
189
190
    /**
191
     * @When /^I should see a "([^"]*)" tab in the CMS content header tabs$/
192
     */
193
    public function stepIShouldSeeInCMSContentTabs($text)
194
    {
195
        // Wait until visible
196
        assertNotNull($this->getCmsTabElement($text), sprintf('%s content tab not found', $text));
197
    }
198
199
    /**
200
     * Applies a specific action to an element
201
     *
202
     * @param NodeElement $element Element to act on
203
     * @param string $action Action, which may be one of 'hover', 'double click', 'right click', or 'left click'
204
     * The default 'click' behaves the same as left click
205
     */
206
    protected function interactWithElement($element, $action = 'click')
207
    {
208
        switch ($action) {
209
            case 'hover':
210
                $element->mouseOver();
211
                break;
212
            case 'double click':
213
                $element->doubleClick();
214
                break;
215
            case 'right click':
216
                $element->rightClick();
217
                break;
218
            case 'left click':
219
            case 'click':
220
            default:
221
                $element->click();
222
                break;
223
        }
224
    }
225
226
    /**
227
     * @When /^I (?P<method>(?:(?:double |right |left |)click)|hover) on "(?P<link>[^"]*)" in the context menu/
228
     */
229
    public function stepIClickOnElementInTheContextMenu($method, $link)
230
    {
231
        $context = $this->getMainContext();
232
        // Wait until context menu has appeared
233
        $this->getSession()->wait(
234
            1000,
235
            "window.jQuery && window.jQuery('.jstree-apple-context').size() > 0"
236
        );
237
        $regionObj = $context->getRegionObj('.jstree-apple-context');
238
        assertNotNull($regionObj, "Context menu could not be found");
239
240
        $linkObj = $regionObj->findLink($link);
241
        if (empty($linkObj)) {
242
            throw new \Exception(sprintf(
243
                'The link "%s" was not found in the context menu on the page %s',
244
                $link,
245
                $this->getSession()->getCurrentUrl()
246
            ));
247
        }
248
249
        $this->interactWithElement($linkObj, $method);
250
    }
251
252
    /**
253
     * @When /^I (?P<method>(?:(?:double |right |left |)click)|hover) on "(?P<text>[^"]*)" in the tree$/
254
     */
255
    public function stepIClickOnElementInTheTree($method, $text)
256
    {
257
        $treeEl = $this->getCmsTreeElement();
258
        $treeNode = $treeEl->findLink($text);
259
        assertNotNull($treeNode, sprintf('%s not found', $text));
260
        $this->interactWithElement($treeNode, $method);
261
    }
262
263
    /**
264
     * @When /^I (?P<method>(?:(?:double |right |left |)click)|hover) on "(?P<text>[^"]*)" in the header tabs$/
265
     */
266
    public function stepIClickOnElementInTheHeaderTabs($method, $text)
267
    {
268
        $tabsNode = $this->getCmsTabElement($text);
269
        assertNotNull($tabsNode, sprintf('%s not found', $text));
270
        $this->interactWithElement($tabsNode, $method);
271
    }
272
273
    /**
274
     * @Then the :text header tab should be active
275
     */
276
    public function theHeaderTabShouldBeActive($text)
277
    {
278
        $element = $this->getCmsTabElement($text);
279
        assertNotNull($element);
280
        assertTrue($element->hasClass('active'));
281
    }
282
283
    /**
284
     * @Then the :text header tab should not be active
285
     */
286
    public function theHeaderTabShouldNotBeActive($text)
287
    {
288
        $element = $this->getCmsTabElement($text);
289
        assertNotNull($element);
290
        assertFalse($element->hasClass('active'));
291
    }
292
293
    /**
294
     * @return NodeElement
295
     */
296
    protected function getCmsTabElement($text)
297
    {
298
        return $this->getCmsTabsElement()->findLink($text);
299
    }
300
301
    /**
302
     * @When /^I expand the "([^"]*)" CMS Panel$/
303
     */
304
    public function iExpandTheCmsPanel()
305
    {
306
        //Tries to find the first visiable toggle in the page
307
        $page = $this->getSession()->getPage();
308
        $toggle_elements = $page->findAll('css', '.toggle-expand');
309
        assertNotNull($toggle_elements, 'Panel toggle not found');
310
        /** @var NodeElement $toggle */
311
        foreach ($toggle_elements as $toggle) {
312
            if ($toggle->isVisible()) {
313
                $toggle->click();
314
            }
315
        }
316
    }
317
318
    /**
319
     * @When /^I (expand|collapse) the content filters$/
320
     */
321
    public function iExpandTheContentFilters($action)
322
    {
323
        $page = $this->getSession()->getPage();
324
        $filterButton = $page->find('css', '#filters-button');
325
        assertNotNull($filterButton, sprintf('Filter button link not found'));
326
327
        $filterButtonCssClass = $filterButton->getAttribute('class');
328
329
        if ($action === 'expand') {
330
            if (strpos($filterButtonCssClass, 'active') === false) {
331
                $filterButton->click();
332
            }
333
        } else {
334
            if (strpos($filterButtonCssClass, 'active') !== false) {
335
                $filterButton->click();
336
            }
337
        }
338
339
        $this->getSession()->wait(2000, 'window.jQuery(".cms-content-filters:animated").length === 0');
340
341
        // If activating, wait until chosen is activated
342
        if ($action === 'expand') {
343
            $this->getSession()->wait(
344
                2000,
345
                <<<'SCRIPT'
346
(window.jQuery(".cms-content-filters select").length === 0) ||
347
(window.jQuery(".cms-content-filters select:visible.has-chosen").length > 0)
348
SCRIPT
349
            );
350
        }
351
    }
352
353
    /**
354
     * @When /^I (expand|collapse) "([^"]*)" in the tree$/
355
     */
356
    public function iExpandInTheTree($action, $nodeText)
357
    {
358
        //Tries to find the first visiable matched Node in the page
359
        $treeEl = $this->getCmsTreeElement();
360
        $treeNode = $treeEl->findLink($nodeText);
361
        assertNotNull($treeNode, sprintf('%s link not found', $nodeText));
362
        $cssIcon = $treeNode->getParent()->getAttribute("class");
363
        if ($action == "expand") {
364
            //ensure it is collapsed
365
            if (false === strpos($cssIcon, 'jstree-open')) {
366
                $nodeIcon = $treeNode->getParent()->find('css', '.jstree-icon');
367
                assertTrue($nodeIcon->isVisible(), "CMS node '$nodeText' not found");
368
                $nodeIcon->click();
369
            }
370
        } else {
371
            //ensure it is expanded
372
            if (false === strpos($cssIcon, 'jstree-closed')) {
373
                $nodeIcon = $treeNode->getParent()->find('css', '.jstree-icon');
374
                assertTrue($nodeIcon->isVisible(), "CMS node '$nodeText' not found");
375
                $nodeIcon->click();
376
            }
377
        }
378
    }
379
380
    /**
381
     * @When /^I should (not |)see a "([^"]*)" CMS tab$/
382
     */
383
    public function iShouldSeeACmsTab($negate, $tab)
384
    {
385
        $this->getSession()->wait(
386
            5000,
387
            "window.jQuery && window.jQuery('.ui-tabs-nav').size() > 0"
388
        );
389
390
        $page = $this->getSession()->getPage();
391
        $tabsets = $page->findAll('css', '.ui-tabs-nav');
392
        assertNotNull($tabsets, 'CMS tabs not found');
393
394
        $tab_element = null;
395
        /** @var NodeElement $tabset */
396
        foreach ($tabsets as $tabset) {
397
            $tab_element = $tabset->find('named', array('link_or_button', "'$tab'"));
398
            if ($tab_element) {
399
                break;
400
            }
401
        }
402
        if ($negate) {
403
            assertNull($tab_element, sprintf('%s tab found', $tab));
404
        } else {
405
            assertNotNull($tab_element, sprintf('%s tab not found', $tab));
406
        }
407
    }
408
409
    /**
410
     * @When /^I click the "([^"]*)" CMS tab$/
411
     */
412
    public function iClickTheCmsTab($tab)
413
    {
414
        $this->getSession()->wait(
415
            5000,
416
            "window.jQuery && window.jQuery('.ui-tabs-nav').size() > 0"
417
        );
418
419
        $page = $this->getSession()->getPage();
420
        $tabsets = $page->findAll('css', '.ui-tabs-nav');
421
        assertNotNull($tabsets, 'CMS tabs not found');
422
423
        $tab_element = null;
424
        /** @var NodeElement $tabset */
425
        foreach ($tabsets as $tabset) {
426
            if ($tab_element) {
427
                continue;
428
            }
429
            $tab_element = $tabset->find('named', array('link_or_button', "'$tab'"));
430
        }
431
        assertNotNull($tab_element, sprintf('%s tab not found', $tab));
432
433
        $tab_element->click();
434
    }
435
436
    /**
437
     * @Then /^I can see the preview panel$/
438
     */
439
    public function iCanSeeThePreviewPanel()
440
    {
441
        $this->getMainContext()->assertElementOnPage('.cms-preview');
442
    }
443
444
    /**
445
     * @Given /^the preview contains "([^"]*)"$/
446
     */
447
    public function thePreviewContains($content)
448
    {
449
        // see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
450
        $this->getSession()->switchToIFrame('cms-preview-iframe');
451
        $this->getMainContext()->assertPageContainsText($content);
452
        $this->getSession()->switchToWindow();
453
    }
454
455
    /**
456
     * @Given /^I set the CMS mode to "([^"]*)"$/
457
     */
458
    public function iSetTheCmsToMode($mode)
459
    {
460
        $this->theIFillInTheDropdownWith('Change view mode', $mode);
461
        sleep(1);
462
    }
463
464
    /**
465
     * @Given /^I wait for the preview to load$/
466
     */
467
    public function iWaitForThePreviewToLoad()
468
    {
469
        // see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
470
        $this->getSession()->switchToIFrame('cms-preview-iframe');
471
        $this->getSession()->wait(
472
            5000,
473
            "window.jQuery && !window.jQuery('iframe[name=cms-preview-iframe]').hasClass('loading')"
474
        );
475
        $this->getSession()->switchToWindow();
476
    }
477
478
    /**
479
     * @Given /^I switch the preview to "([^"]*)"$/
480
     */
481
    public function iSwitchThePreviewToMode($mode)
482
    {
483
        $controls = $this->getSession()->getPage()->find('css', '.cms-preview-controls');
484
        assertNotNull($controls, 'Preview controls not found');
485
486
        $label = $controls->find('xpath', sprintf(
487
            './/*[count(*)=0 and contains(text(), \'%s\')]',
488
            $mode
489
        ));
490
        assertNotNull($label, 'Preview mode switch not found');
491
492
        $label->click();
493
494
        $this->iWaitForThePreviewToLoad();
495
    }
496
497
    /**
498
     * @Given /^the preview does not contain "([^"]*)"$/
499
     */
500
    public function thePreviewDoesNotContain($content)
501
    {
502
        // see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
503
        $this->getSession()->switchToIFrame('cms-preview-iframe');
504
        $this->getMainContext()->assertPageNotContainsText($content);
505
        $this->getSession()->switchToWindow();
506
    }
507
508
    /**
509
     * When I follow "my link" in preview
510
     *
511
     * @When /^(?:|I )follow "(?P<link>(?:[^"]|\\")*)" in preview$/
512
     */
513
    public function clickLinkInPreview($link)
514
    {
515
        // TODO Remove once we have native support in Mink and php-webdriver,
516
        // see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
517
        $this->getSession()->switchToIFrame('cms-preview-iframe');
518
        $link = $this->fixStepArgument($link);
519
        $this->getSession()->getPage()->clickLink($link);
520
        $this->getSession()->switchToWindow();
521
    }
522
523
    /**
524
     * When I press "submit" in preview
525
     *
526
     * @When /^(?:|I )press "(?P<button>(?:[^"]|\\")*)" in preview$/
527
     */
528
    public function pressButtonInPreview($button)
529
    {
530
        // see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
531
        $this->getSession()->switchToIFrame('cms-preview-iframe');
532
        $button = $this->fixStepArgument($button);
533
        $this->getSession()->getPage()->pressButton($button);
534
        $this->getSession()->switchToWindow();
535
    }
536
537
    /**
538
     * Workaround for chosen.js dropdowns or tree dropdowns which hide the original dropdown field.
539
     *
540
     * @When /^(?:|I )fill in the "(?P<field>(?:[^"]|\\")*)" dropdown with "(?P<value>(?:[^"]|\\")*)"$/
541
     * @When /^(?:|I )fill in "(?P<value>(?:[^"]|\\")*)" for the "(?P<field>(?:[^"]|\\")*)" dropdown$/
542
     */
543
    public function theIFillInTheDropdownWith($field, $value)
544
    {
545
        $field = $this->fixStepArgument($field);
546
        $value = $this->fixStepArgument($value);
547
548
        $escaper = new Escaper();
549
        $nativeField = $this->getSession()->getPage()->find(
550
            'named',
551
            array('select', $escaper->escapeLiteral($field))
552
        );
553
        if ($nativeField && $nativeField->isVisible()) {
554
            $nativeField->selectOption($value);
555
            return;
556
        }
557
558
        // Given the fuzzy matching, we might get more than one matching field.
559
        $formFields = array();
560
561
        // Find by label
562
        $formField = $this->getSession()->getPage()->findField($field);
563
        if ($formField && $formField->getTagName() == 'select') {
564
            $formFields[] = $formField;
565
        }
566
567
        // Fall back to finding by title (for dropdowns without a label)
568
        if (!$formFields) {
569
            $formFields = $this->getSession()->getPage()->findAll(
570
                'xpath',
571
                sprintf(
572
                    '//*[self::select][(./@title="%s")]',
573
                    $field
574
                )
575
            );
576
        }
577
578
        // Find by name (incl. hidden fields)
579
        if (!$formFields) {
580
            $formFields = $this->getSession()->getPage()->findAll('xpath', "//*[@name='$field']");
581
        }
582
583
        // Find by label
584
        if (!$formFields) {
585
            $label = $this->getSession()->getPage()->find('xpath', "//label[.='$field']");
586
            if ($label && $for = $label->getAttribute('for')) {
587
                $formField = $this->getSession()->getPage()->find('xpath', "//*[@id='$for']");
588
                if ($formField) {
589
                    $formFields[] = $formField;
590
                }
591
            }
592
        }
593
594
        assertGreaterThan(0, count($formFields), sprintf(
595
            'Chosen.js dropdown named "%s" not found',
596
            $field
597
        ));
598
599
        // Traverse up to field holder
600
        /** @var NodeElement $container */
601
        $container = null;
602
        foreach ($formFields as $formField) {
603
            $container = $this->findParentByClass($formField, 'field');
604
            if ($container) {
605
                break; // Default to first visible container
606
            }
607
        }
608
609
        assertNotNull($container, 'Chosen.js field container not found');
610
611
        // Click on newly expanded list element, indirectly setting the dropdown value
612
        $linkEl = $container->find('xpath', './/a');
613
        assertNotNull($linkEl, 'Chosen.js link element not found');
614
        $this->getSession()->wait(100); // wait for dropdown overlay to appear
615
        $linkEl->click();
616
617
        if (in_array('treedropdown', explode(' ', $container->getAttribute('class')))) {
618
            // wait for ajax dropdown to load
619
            $this->getSession()->wait(
620
                5000,
621
                "window.jQuery && "
622
                . "window.jQuery('#" . $container->getAttribute('id') . " .treedropdownfield-panel li').length > 0"
623
            );
624
        } else {
625
            // wait for dropdown overlay to appear (might be animated)
626
            $this->getSession()->wait(300);
627
        }
628
629
        $listEl = $container->find('xpath', sprintf('.//li[contains(normalize-space(string(.)), \'%s\')]', $value));
630
        if (null === $listEl) {
631
            throw new \InvalidArgumentException(sprintf(
632
                'Chosen.js list element with title "%s" not found',
633
                $value
634
            ));
635
        }
636
637
        $listLinkEl = $listEl->find('xpath', './/a');
638
        if ($listLinkEl) {
639
            $listLinkEl->click();
640
        } else {
641
            $listEl->click();
642
        }
643
    }
644
645
    /**
646
     * Returns fixed step argument (with \\" replaced back to ").
647
     *
648
     * @param string $argument
649
     *
650
     * @return string
651
     */
652
    protected function fixStepArgument($argument)
653
    {
654
        return str_replace('\\"', '"', $argument);
655
    }
656
657
    /**
658
     * Returns the closest parent element having a specific class attribute.
659
     *
660
     * @param  NodeElement $el
661
     * @param  String  $class
662
     * @return Element|null
663
     */
664
    protected function findParentByClass(NodeElement $el, $class)
665
    {
666
        $container = $el->getParent();
667
        while ($container && $container->getTagName() != 'body') {
668
            if ($container->isVisible() && in_array($class, explode(' ', $container->getAttribute('class')))) {
669
                return $container;
670
            }
671
            $container = $container->getParent();
672
        }
673
674
        return null;
675
    }
676
}
677