Passed
Push — 4 ( e4c1ce...cc2202 )
by Luke
07:43
created

CmsUiContext::iPressTheKeyInTheField()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 2
dl 0
loc 9
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', '.search-box__filter-trigger');
325
        assertNotNull($filterButton, sprintf('Filter button link not found'));
326
327
        $filterButtonExpanded = $filterButton->getAttribute('aria-expanded');
328
329
        if ($action === 'expand') {
330
            if ($filterButtonExpanded === false) {
331
                $filterButton->click();
332
            }
333
        } else {
334
            if ($filterButtonExpanded === true) {
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
     * @Given /^I press the "([^"]*)" key in the "([^"]*)" field$/
355
     */
356
    public function iPressTheKeyInTheField($key, $field)
357
    {
358
        $this->getSession()->evaluateScript(sprintf(
359
            "jQuery('[name=\"%s\"]')[0].dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: \"%s\" }));
360
            jQuery('[name=\"%s\"]')[0].dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: \"%s\" }));",
361
            $field,
362
            $key,
363
            $field,
364
            $key
365
        ));
366
    }
367
368
    /**
369
     * @When /^I (expand|collapse) "([^"]*)" in the tree$/
370
     */
371
    public function iExpandInTheTree($action, $nodeText)
372
    {
373
        //Tries to find the first visiable matched Node in the page
374
        $treeEl = $this->getCmsTreeElement();
375
        $treeNode = $treeEl->findLink($nodeText);
376
        assertNotNull($treeNode, sprintf('%s link not found', $nodeText));
377
        $cssIcon = $treeNode->getParent()->getAttribute("class");
378
        if ($action == "expand") {
379
            //ensure it is collapsed
380
            if (false === strpos($cssIcon, 'jstree-open')) {
381
                $nodeIcon = $treeNode->getParent()->find('css', '.jstree-icon');
382
                assertTrue($nodeIcon->isVisible(), "CMS node '$nodeText' not found");
383
                $nodeIcon->click();
384
            }
385
        } else {
386
            //ensure it is expanded
387
            if (false === strpos($cssIcon, 'jstree-closed')) {
388
                $nodeIcon = $treeNode->getParent()->find('css', '.jstree-icon');
389
                assertTrue($nodeIcon->isVisible(), "CMS node '$nodeText' not found");
390
                $nodeIcon->click();
391
            }
392
        }
393
    }
394
395
    /**
396
     * @When /^I should (not |)see a "([^"]*)" CMS tab$/
397
     */
398
    public function iShouldSeeACmsTab($negate, $tab)
399
    {
400
        $this->getSession()->wait(
401
            5000,
402
            "window.jQuery && window.jQuery('.ui-tabs-nav').size() > 0"
403
        );
404
405
        $page = $this->getSession()->getPage();
406
        $tabsets = $page->findAll('css', '.ui-tabs-nav');
407
        assertNotNull($tabsets, 'CMS tabs not found');
408
409
        $tab_element = null;
410
        /** @var NodeElement $tabset */
411
        foreach ($tabsets as $tabset) {
412
            $tab_element = $tabset->find('named', array('link_or_button', "'$tab'"));
413
            if ($tab_element) {
414
                break;
415
            }
416
        }
417
        if ($negate) {
418
            assertNull($tab_element, sprintf('%s tab found', $tab));
419
        } else {
420
            assertNotNull($tab_element, sprintf('%s tab not found', $tab));
421
        }
422
    }
423
424
    /**
425
     * @When /^I click the "([^"]*)" CMS tab$/
426
     */
427
    public function iClickTheCmsTab($tab)
428
    {
429
        $this->getSession()->wait(
430
            5000,
431
            "window.jQuery && window.jQuery('.ui-tabs-nav').size() > 0"
432
        );
433
434
        $page = $this->getSession()->getPage();
435
        $tabsets = $page->findAll('css', '.ui-tabs-nav');
436
        assertNotNull($tabsets, 'CMS tabs not found');
437
438
        $tab_element = null;
439
        /** @var NodeElement $tabset */
440
        foreach ($tabsets as $tabset) {
441
            if ($tab_element) {
442
                continue;
443
            }
444
            $tab_element = $tabset->find('named', array('link_or_button', "'$tab'"));
445
        }
446
        assertNotNull($tab_element, sprintf('%s tab not found', $tab));
447
448
        $tab_element->click();
449
    }
450
451
    /**
452
     * @Then /^I can see the preview panel$/
453
     */
454
    public function iCanSeeThePreviewPanel()
455
    {
456
        $this->getMainContext()->assertElementOnPage('.cms-preview');
457
    }
458
459
    /**
460
     * @Given /^the preview contains "([^"]*)"$/
461
     */
462
    public function thePreviewContains($content)
463
    {
464
        // see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
465
        $this->getSession()->switchToIFrame('cms-preview-iframe');
466
        $this->getMainContext()->assertPageContainsText($content);
467
        $this->getSession()->switchToWindow();
468
    }
469
470
    /**
471
     * @Given /^I set the CMS mode to "([^"]*)"$/
472
     */
473
    public function iSetTheCmsToMode($mode)
474
    {
475
        $this->theIFillInTheDropdownWith('Change view mode', $mode);
476
        sleep(1);
477
    }
478
479
    /**
480
     * @Given /^I wait for the preview to load$/
481
     */
482
    public function iWaitForThePreviewToLoad()
483
    {
484
        // see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
485
        $this->getSession()->switchToIFrame('cms-preview-iframe');
486
        $this->getSession()->wait(
487
            5000,
488
            "window.jQuery && !window.jQuery('iframe[name=cms-preview-iframe]').hasClass('loading')"
489
        );
490
        $this->getSession()->switchToWindow();
491
    }
492
493
    /**
494
     * @Given /^I switch the preview to "([^"]*)"$/
495
     */
496
    public function iSwitchThePreviewToMode($mode)
497
    {
498
        $controls = $this->getSession()->getPage()->find('css', '.cms-preview-controls');
499
        assertNotNull($controls, 'Preview controls not found');
500
501
        $label = $controls->find('xpath', sprintf(
502
            './/*[count(*)=0 and contains(text(), \'%s\')]',
503
            $mode
504
        ));
505
        assertNotNull($label, 'Preview mode switch not found');
506
507
        $label->click();
508
509
        $this->iWaitForThePreviewToLoad();
510
    }
511
512
    /**
513
     * @Given /^the preview does not contain "([^"]*)"$/
514
     */
515
    public function thePreviewDoesNotContain($content)
516
    {
517
        // see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
518
        $this->getSession()->switchToIFrame('cms-preview-iframe');
519
        $this->getMainContext()->assertPageNotContainsText($content);
520
        $this->getSession()->switchToWindow();
521
    }
522
523
    /**
524
     * When I follow "my link" in preview
525
     *
526
     * @When /^(?:|I )follow "(?P<link>(?:[^"]|\\")*)" in preview$/
527
     */
528
    public function clickLinkInPreview($link)
529
    {
530
        // TODO Remove once we have native support in Mink and php-webdriver,
531
        // see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
532
        $this->getSession()->switchToIFrame('cms-preview-iframe');
533
        $link = $this->fixStepArgument($link);
534
        $this->getSession()->getPage()->clickLink($link);
535
        $this->getSession()->switchToWindow();
536
    }
537
538
    /**
539
     * When I press "submit" in preview
540
     *
541
     * @When /^(?:|I )press "(?P<button>(?:[^"]|\\")*)" in preview$/
542
     */
543
    public function pressButtonInPreview($button)
544
    {
545
        // see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
546
        $this->getSession()->switchToIFrame('cms-preview-iframe');
547
        $button = $this->fixStepArgument($button);
548
        $this->getSession()->getPage()->pressButton($button);
549
        $this->getSession()->switchToWindow();
550
    }
551
552
    /**
553
     * Workaround for chosen.js dropdowns or tree dropdowns which hide the original dropdown field.
554
     *
555
     * @When /^(?:|I )fill in the "(?P<field>(?:[^"]|\\")*)" dropdown with "(?P<value>(?:[^"]|\\")*)"$/
556
     * @When /^(?:|I )fill in "(?P<value>(?:[^"]|\\")*)" for the "(?P<field>(?:[^"]|\\")*)" dropdown$/
557
     */
558
    public function theIFillInTheDropdownWith($field, $value)
559
    {
560
        $field = $this->fixStepArgument($field);
561
        $value = $this->fixStepArgument($value);
562
563
        $escaper = new Escaper();
564
        $nativeField = $this->getSession()->getPage()->find(
565
            'named',
566
            array('select', $escaper->escapeLiteral($field))
567
        );
568
        if ($nativeField && $nativeField->isVisible()) {
569
            $nativeField->selectOption($value);
570
            return;
571
        }
572
573
        // Given the fuzzy matching, we might get more than one matching field.
574
        $formFields = array();
575
576
        // Find by label
577
        $formField = $this->getSession()->getPage()->findField($field);
578
        if ($formField && $formField->getTagName() == 'select') {
579
            $formFields[] = $formField;
580
        }
581
582
        // Fall back to finding by title (for dropdowns without a label)
583
        if (!$formFields) {
584
            $formFields = $this->getSession()->getPage()->findAll(
585
                'xpath',
586
                sprintf(
587
                    '//*[self::select][(./@title="%s")]',
588
                    $field
589
                )
590
            );
591
        }
592
593
        // Find by name (incl. hidden fields)
594
        if (!$formFields) {
595
            $formFields = $this->getSession()->getPage()->findAll('xpath', "//*[@name='$field']");
596
        }
597
598
        // Find by label
599
        if (!$formFields) {
600
            $label = $this->getSession()->getPage()->find('xpath', "//label[.='$field']");
601
            if ($label && $for = $label->getAttribute('for')) {
602
                $formField = $this->getSession()->getPage()->find('xpath', "//*[@id='$for']");
603
                if ($formField) {
604
                    $formFields[] = $formField;
605
                }
606
            }
607
        }
608
609
        assertGreaterThan(0, count($formFields), sprintf(
610
            'Chosen.js dropdown named "%s" not found',
611
            $field
612
        ));
613
614
        // Traverse up to field holder
615
        /** @var NodeElement $container */
616
        $container = null;
617
        foreach ($formFields as $formField) {
618
            $container = $this->findParentByClass($formField, 'field');
619
            if ($container) {
620
                break; // Default to first visible container
621
            }
622
        }
623
624
        assertNotNull($container, 'Chosen.js field container not found');
625
626
        // Click on newly expanded list element, indirectly setting the dropdown value
627
        $linkEl = $container->find('xpath', './/a');
628
        assertNotNull($linkEl, 'Chosen.js link element not found');
629
        $this->getSession()->wait(100); // wait for dropdown overlay to appear
630
        $linkEl->click();
631
632
        if (in_array('treedropdown', explode(' ', $container->getAttribute('class')))) {
633
            // wait for ajax dropdown to load
634
            $this->getSession()->wait(
635
                5000,
636
                "window.jQuery && "
637
                . "window.jQuery('#" . $container->getAttribute('id') . " .treedropdownfield-panel li').length > 0"
638
            );
639
        } else {
640
            // wait for dropdown overlay to appear (might be animated)
641
            $this->getSession()->wait(300);
642
        }
643
644
        $listEl = $container->find('xpath', sprintf('.//li[contains(normalize-space(string(.)), \'%s\')]', $value));
645
        if (null === $listEl) {
646
            throw new \InvalidArgumentException(sprintf(
647
                'Chosen.js list element with title "%s" not found',
648
                $value
649
            ));
650
        }
651
652
        $listLinkEl = $listEl->find('xpath', './/a');
653
        if ($listLinkEl) {
654
            $listLinkEl->click();
655
        } else {
656
            $listEl->click();
657
        }
658
    }
659
660
    /**
661
     * Returns fixed step argument (with \\" replaced back to ").
662
     *
663
     * @param string $argument
664
     *
665
     * @return string
666
     */
667
    protected function fixStepArgument($argument)
668
    {
669
        return str_replace('\\"', '"', $argument);
670
    }
671
672
    /**
673
     * Returns the closest parent element having a specific class attribute.
674
     *
675
     * @param  NodeElement $el
676
     * @param  String  $class
677
     * @return Element|null
678
     */
679
    protected function findParentByClass(NodeElement $el, $class)
680
    {
681
        $container = $el->getParent();
682
        while ($container && $container->getTagName() != 'body') {
683
            if ($container->isVisible() && in_array($class, explode(' ', $container->getAttribute('class')))) {
684
                return $container;
685
            }
686
            $container = $container->getParent();
687
        }
688
689
        return null;
690
    }
691
}
692