Completed
Push — 3.7 ( 81b2d8...ef0909 )
by
unknown
09:42
created

CmsUiContext::interactWithElement()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 6
nop 2
dl 0
loc 19
rs 9.0111
c 0
b 0
f 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 22 and the first side effect is on line 14.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
namespace SilverStripe\Framework\Test\Behaviour;
4
5
use Behat\Behat\Context\BehatContext,
6
	Behat\Behat\Context\Step,
7
	Behat\Behat\Event\StepEvent,
8
	Behat\Mink\Element\NodeElement,
9
	Behat\Mink\Session,
10
	WebDriver\Exception\UnexpectedAlertOpen;
11
12
13
// PHPUnit
14
require_once 'PHPUnit/Autoload.php';
15
require_once 'PHPUnit/Framework/Assert/Functions.php';
16
17
/**
18
 * CmsUiContext
19
 *
20
 * Context used to define steps related to SilverStripe CMS UI like Tree or Panel.
21
 */
22
class CmsUiContext extends BehatContext {
23
	protected $context;
24
25
	/**
26
	 * Initializes context.
27
	 * Every scenario gets it's own context object.
28
	 *
29
	 * @param   array   $parameters     context parameters (set them up through behat.yml)
30
	 */
31
	public function __construct(array $parameters) {
32
		// Initialize your context here
33
		$this->context = $parameters;
34
	}
35
36
	/**
37
	 * Get Mink session from MinkContext
38
	 *
39
	 * @return Session
40
	 */
41
	public function getSession($name = null) {
42
		return $this->getMainContext()->getSession($name);
43
	}
44
45
	/**
46
	 * Wait until CMS loading overlay isn't present.
47
	 * This is an addition to the "ajax steps" logic in
48
	 * SilverStripe\BehatExtension\Context\BasicContext
49
	 * which also waits for any ajax requests to finish before continuing.
50
	 *
51
	 * The check also applies in when not in the CMS, which is a structural issue:
52
	 * Every step could cause the CMS to be loaded, and we don't know if we're in the
53
	 * CMS UI until we run a check.
54
	 *
55
	 * Excluding scenarios with @modal tag is required,
56
	 * because modal dialogs stop any JS interaction
57
	 *
58
	 * @AfterStep ~@modal
59
	 */
60
	public function handleCmsLoadingAfterStep(StepEvent $event) {
61
		$timeoutMs = $this->getMainContext()->getAjaxTimeout();
62
		$this->getSession()->wait($timeoutMs,
63
            "document.getElementsByClassName('cms-content-loading-overlay').length == 0"
64
        );
65
	}
66
67
	/**
68
	 * @Then /^I should see the CMS$/
69
	 */
70
	public function iShouldSeeTheCms() {
71
		$page = $this->getSession()->getPage();
72
		$cms_element = $page->find('css', '.cms');
73
		assertNotNull($cms_element, 'CMS not found');
74
	}
75
76
	/**
77
	 * @Then /^I should see a "([^"]*)" notice$/
78
	 */
79
	public function iShouldSeeANotice($notice) {
80
		$this->getMainContext()->assertElementContains('.notice-wrap', $notice);
81
	}
82
83
	/**
84
	 * @Then /^I should see a "([^"]*)" message$/
85
	 */
86
	public function iShouldSeeAMessage($message) {
87
		$this->getMainContext()->assertElementContains('.message', $message);
88
	}
89
90
	protected function getCmsTabsElement() {
91
		$this->getSession()->wait(
92
			5000,
93
			"window.jQuery && window.jQuery('.cms-content-header-tabs').size() > 0"
94
		);
95
96
		$page = $this->getSession()->getPage();
97
		$cms_content_header_tabs = $page->find('css', '.cms-content-header-tabs');
98
		assertNotNull($cms_content_header_tabs, 'CMS tabs not found');
99
100
		return $cms_content_header_tabs;
101
	}
102
103
	protected function getCmsContentToolbarElement() {
104
		$this->getSession()->wait(
105
			5000,
106
			"window.jQuery && window.jQuery('.cms-content-toolbar').size() > 0 "
107
			. "&& window.jQuery('.cms-content-toolbar').children().size() > 0"
108
		);
109
110
		$page = $this->getSession()->getPage();
111
		$cms_content_toolbar_element = $page->find('css', '.cms-content-toolbar');
112
		assertNotNull($cms_content_toolbar_element, 'CMS content toolbar not found');
113
114
		return $cms_content_toolbar_element;
115
	}
116
117
	protected function getCmsTreeElement() {
118
		$this->getSession()->wait(
119
			5000,
120
			"window.jQuery && window.jQuery('.cms-tree').size() > 0"
121
		);
122
123
		$page = $this->getSession()->getPage();
124
		$cms_tree_element = $page->find('css', '.cms-tree');
125
		assertNotNull($cms_tree_element, 'CMS tree not found');
126
127
		return $cms_tree_element;
128
	}
129
130
	/**
131
	 * @Given /^I should see a "([^"]*)" button in CMS Content Toolbar$/
132
	 */
133
	public function iShouldSeeAButtonInCmsContentToolbar($text) {
134
		$cms_content_toolbar_element = $this->getCmsContentToolbarElement();
135
136
		$element = $cms_content_toolbar_element->find('named', array('link_or_button', "'$text'"));
137
		assertNotNull($element, sprintf('%s button not found', $text));
138
	}
139
140
	/**
141
	 * @When /^I should see "([^"]*)" in the tree$/
142
	 */
143
	public function stepIShouldSeeInCmsTree($text) {
144
		$cms_tree_element = $this->getCmsTreeElement();
145
146
		$element = $cms_tree_element->find('named', array('content', "'$text'"));
147
		assertNotNull($element, sprintf('%s not found', $text));
148
	}
149
150
	/**
151
	 * @When /^I should not see "([^"]*)" in the tree$/
152
	 */
153
	public function stepIShouldNotSeeInCmsTree($text) {
154
		$cms_tree_element = $this->getCmsTreeElement();
155
156
		$element = $cms_tree_element->find('named', array('content', "'$text'"));
157
		assertNull($element, sprintf('%s found', $text));
158
	}
159
160
	/**
161
	 * Applies a specific action to an element
162
	 *
163
	 * @param NodeElement $element Element to act on
164
	 * @param string $action Action, which may be one of 'hover', 'double click', 'right click', or 'left click'
165
	 * The default 'click' behaves the same as left click
166
	 */
167
	protected function interactWithElement($element, $action = 'click') {
168
		switch($action) {
169
			case 'hover':
170
				$element->mouseOver();
171
				break;
172
			case 'double click':
173
				$element->doubleClick();
174
				break;
175
			case 'right click':
176
				$element->rightClick();
177
				break;
178
			case 'left click':
179
			case 'click':
180
			default:
181
				$element->click();
182
				break;
183
		}
184
185
	}
186
187
	/**
188
	 * @When /^I (?P<method>(?:(?:double |right |left |)click)|hover) on "(?P<link>[^"]*)" in the context menu/
189
	 */
190
	public function stepIClickOnElementInTheContextMenu($method, $link) {
191
		$context = $this->getMainContext();
192
		// Wait until context menu has appeared
193
		$this->getSession()->wait(
194
			1000,
195
			"window.jQuery && window.jQuery('.jstree-apple-context').size() > 0"
196
		);
197
		$regionObj = $context->getRegionObj('.jstree-apple-context');
198
		assertNotNull($regionObj, "Context menu could not be found");
199
200
		$linkObj = $regionObj->findLink($link);
201
		if (empty($linkObj)) {
202
			throw new \Exception(sprintf(
203
				'The link "%s" was not found in the context menu on the page %s',
204
				$link,
205
				$this->getSession()->getCurrentUrl()
206
			));
207
		}
208
209
		$this->interactWithElement($linkObj, $method);
210
	}
211
212
	/**
213
	 * @When /^I (?P<method>(?:(?:double |right |left |)click)|hover) on "(?P<text>[^"]*)" in the tree$/
214
	 */
215
	public function stepIClickOnElementInTheTree($method, $text) {
216
		$treeEl = $this->getCmsTreeElement();
217
		$treeNode = $treeEl->findLink($text);
218
		assertNotNull($treeNode, sprintf('%s not found', $text));
219
		$this->interactWithElement($treeNode, $method);
220
	}
221
222
	/**
223
	 * @When /^I expand the "([^"]*)" CMS Panel$/
224
	 */
225
	public function iExpandTheCmsPanel() {
226
		//Tries to find the first visiable toggle in the page
227
		$page = $this->getSession()->getPage();
228
		$toggle_elements = $page->findAll('css', '.toggle-expand');
229
		assertNotNull($toggle_elements, 'Panel toggle not found');
230
		foreach($toggle_elements as $toggle){
231
			if($toggle->isVisible()){
232
				$toggle->click();
233
			}
234
		}
235
	}
236
237
	/**
238
	 * @When /^I (expand|collapse) the content filters$/
239
	 */
240
	public function iExpandTheContentFilters($action) {
241
		$page = $this->getSession()->getPage();
242
		$filterButton = $page->find('css', '#filters-button');
243
		assertNotNull($filterButton, sprintf('Filter button link not found'));
244
245
		$filterButtonCssClass = $filterButton->getAttribute('class');
246
247
		if($action == 'expand') {
248
			if(strpos($filterButtonCssClass, 'active') === false) {
249
				$filterButton->click();
250
			}
251
		} else {
252
			if(strpos($filterButtonCssClass, 'active') !== false) {
253
				$filterButton->click();
254
			}
255
		}
256
257
		$this->getSession()->wait(2000, 'window.jQuery(".cms-content-filters:animated").length === 0');
258
	}
259
260
	/**
261
	 * @When /^I (expand|collapse) "([^"]*)" in the tree$/
262
	 */
263
	public function iExpandInTheTree($action, $nodeText) {
264
		//Tries to find the first visiable matched Node in the page
265
		$page = $this->getSession()->getPage();
266
		$treeEl = $this->getCmsTreeElement();
267
		$treeNode = $treeEl->findLink($nodeText);
268
		assertNotNull($treeNode, sprintf('%s link not found', $nodeText));
269
		$cssIcon = $treeNode->getParent()->getAttribute("class");
270
		if($action == "expand") {
271
			//ensure it is collapsed
272
			if(false === strpos($cssIcon, 'jstree-open')) {
273
				$nodeIcon = $treeNode->getParent()->find('css', '.jstree-icon');
274
				assertTrue($nodeIcon->isVisible(), "CMS node '$nodeText' not found");
275
				$nodeIcon->click();
276
			}
277
		} else {
278
			//ensure it is expanded
279
			if(false === strpos($cssIcon, 'jstree-closed')) {
280
				$nodeIcon = $treeNode->getParent()->find('css', '.jstree-icon');
281
				assertTrue($nodeIcon->isVisible(), "CMS node '$nodeText' not found");
282
				$nodeIcon->click();
283
			}
284
		}
285
	}
286
287
	/**
288
	 * @When /^I should (not |)see a "([^"]*)" CMS tab$/
289
	 */
290
	public function iShouldSeeACmsTab($negate, $tab) {
291
		$this->getSession()->wait(
292
			5000,
293
			"window.jQuery && window.jQuery('.ui-tabs-nav').size() > 0"
294
		);
295
296
		$page = $this->getSession()->getPage();
297
		$tabsets = $page->findAll('css', '.ui-tabs-nav');
298
		assertNotNull($tabsets, 'CMS tabs not found');
299
300
		$tab_element = null;
301
		foreach($tabsets as $tabset) {
302
			$tab_element = $tabset->find('named', array('link_or_button', "'$tab'"));
303
			if($tab_element) break;
304
		}
305
		if($negate) {
306
			assertNull($tab_element, sprintf('%s tab found', $tab));
307
		} else {
308
			assertNotNull($tab_element, sprintf('%s tab not found', $tab));
309
		}
310
	}
311
312
	/**
313
	 * @When /^I click the "([^"]*)" CMS tab$/
314
	 */
315
	public function iClickTheCmsTab($tab) {
316
		$this->getSession()->wait(
317
			5000,
318
			"window.jQuery && window.jQuery('.ui-tabs-nav').size() > 0"
319
		);
320
321
		$page = $this->getSession()->getPage();
322
		$tabsets = $page->findAll('css', '.ui-tabs-nav');
323
		assertNotNull($tabsets, 'CMS tabs not found');
324
325
		$tab_element = null;
326
		foreach($tabsets as $tabset) {
327
			if($tab_element) continue;
328
			$tab_element = $tabset->find('named', array('link_or_button', "'$tab'"));
329
		}
330
		assertNotNull($tab_element, sprintf('%s tab not found', $tab));
331
332
		$tab_element->click();
333
	}
334
335
	/**
336
	 * @Then /^I can see the preview panel$/
337
	 */
338
	public function iCanSeeThePreviewPanel() {
339
		$this->getMainContext()->assertElementOnPage('.cms-preview');
340
	}
341
342
	/**
343
	 * @Given /^the preview contains "([^"]*)"$/
344
	 */
345
	public function thePreviewContains($content) {
346
		$driver = $this->getSession()->getDriver();
347
		// TODO Remove once we have native support in Mink and php-webdriver,
348
		// see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
349
		$origWindowName = $driver->getWebDriverSession()->window_handle();
350
351
		$driver->switchToIFrame('cms-preview-iframe');
352
		$this->getMainContext()->assertPageContainsText($content);
353
		$driver->switchToWindow($origWindowName);
354
	}
355
356
	/**
357
	 * @Given /^I set the CMS mode to "([^"]*)"$/
358
	 */
359
	public function iSetTheCmsToMode($mode) {
360
		return array(
361
			new Step\When(sprintf('I fill in the "Change view mode" dropdown with "%s"', $mode)),
362
			new Step\When('I wait for 1 second') // wait for CMS layout to redraw
363
		);
364
	}
365
366
	/**
367
	 * @Given /^I wait for the preview to load$/
368
	 */
369
	public function iWaitForThePreviewToLoad()  {
370
		$driver = $this->getSession()->getDriver();
371
		// TODO Remove once we have native support in Mink and php-webdriver,
372
		// see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
373
		$origWindowName = $driver->getWebDriverSession()->window_handle();
374
375
		$driver->switchToIFrame('cms-preview-iframe');
376
		$this->getSession()->wait(
377
			5000,
378
			"window.jQuery && !window.jQuery('iframe[name=cms-preview-iframe]').hasClass('loading')"
379
		);
380
		$driver->switchToWindow($origWindowName);
381
	}
382
383
	/**
384
	 * @Given /^I switch the preview to "([^"]*)"$/
385
	 */
386
	public function iSwitchThePreviewToMode($mode)  {
387
		$controls = $this->getSession()->getPage()->find('css', '.cms-preview-controls');
388
		assertNotNull($controls, 'Preview controls not found');
389
390
		$label = $controls->find('xpath', sprintf(
391
			'.//label[(@for="%s")]',
392
			$mode
393
		));
394
		assertNotNull($label, 'Preview mode switch not found');
395
396
		$label->click();
397
398
		return new Step\When('I wait for the preview to load');
399
	}
400
401
	/**
402
	 * @Given /^the preview does not contain "([^"]*)"$/
403
	 */
404
	public function thePreviewDoesNotContain($content) {
405
		$driver = $this->getSession()->getDriver();
406
		// TODO Remove once we have native support in Mink and php-webdriver,
407
		// see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
408
		$origWindowName = $driver->getWebDriverSession()->window_handle();
409
410
		$driver->switchToIFrame('cms-preview-iframe');
411
		$this->getMainContext()->assertPageNotContainsText($content);
412
		$driver->switchToWindow($origWindowName);
413
	}
414
415
	/**
416
	 * When I follow "my link" in preview
417
	 *
418
	 * @When /^(?:|I )follow "(?P<link>(?:[^"]|\\")*)" in preview$/
419
	 */
420
	public function clickLinkInPreview($link) {
421
		$driver = $this->getSession()->getDriver();
422
		// TODO Remove once we have native support in Mink and php-webdriver,
423
		// see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
424
		$origWindowName = $driver->getWebDriverSession()->window_handle();
425
		$driver->switchToIFrame('cms-preview-iframe');
426
427
		$link = $this->fixStepArgument($link);
428
		$this->getSession()->getPage()->clickLink($link);
429
430
		$driver->switchToWindow($origWindowName);
431
	}
432
433
	/**
434
	 * When I press "submit" in preview
435
	 *
436
	 * @When /^(?:|I )press "(?P<button>(?:[^"]|\\")*)" in preview$/
437
	 */
438
	public function pressButtonInPreview($button) {
439
		$driver = $this->getSession()->getDriver();
440
		// TODO Remove once we have native support in Mink and php-webdriver,
441
		// see https://groups.google.com/forum/#!topic/behat/QNhOuGHKEWI
442
		$origWindowName = $driver->getWebDriverSession()->window_handle();
443
		$driver->switchToIFrame('cms-preview-iframe');
444
445
		$button = $this->fixStepArgument($button);
446
		$this->getSession()->getPage()->pressButton($button);
447
448
		$driver->switchToWindow($origWindowName);
449
	}
450
451
	/**
452
	 * Workaround for chosen.js dropdowns or tree dropdowns which hide the original dropdown field.
453
	 *
454
	 * @When /^(?:|I )fill in the "(?P<field>(?:[^"]|\\")*)" dropdown with "(?P<value>(?:[^"]|\\")*)"$/
455
	 * @When /^(?:|I )fill in "(?P<value>(?:[^"]|\\")*)" for the "(?P<field>(?:[^"]|\\")*)" dropdown$/
456
	 */
457
	public function theIFillInTheDropdownWith($field, $value) {
458
		$field = $this->fixStepArgument($field);
459
		$value = $this->fixStepArgument($value);
460
461
		$nativeField = $this->getSession()->getPage()->find(
462
			'named',
463
			array('select', $this->getSession()->getSelectorsHandler()->xpathLiteral($field))
464
		);
465
		if($nativeField && $nativeField->isVisible()) {
466
			$nativeField->selectOption($value);
467
			return;
468
		}
469
470
		// Given the fuzzy matching, we might get more than one matching field.
471
		$formFields = array();
472
473
		// Find by label
474
		$formField = $this->getSession()->getPage()->findField($field);
475
		if($formField && $formField->getTagName() == 'select') {
476
			$formFields[] = $formField;
477
		}
478
479
		// Fall back to finding by title (for dropdowns without a label)
480
		if(!$formFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $formFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
481
			$formFields = $this->getSession()->getPage()->findAll(
482
				'xpath',
483
				sprintf(
484
					'//*[self::select][(./@title="%s")]',
485
					$field
486
				)
487
			);
488
		}
489
490
		// Find by name (incl. hidden fields)
491
		if(!$formFields) {
492
			$formFields = $this->getSession()->getPage()->findAll('xpath', "//*[@name='$field']");
493
		}
494
495
		// Find by label
496
		if(!$formFields) {
497
			$label = $this->getSession()->getPage()->find('xpath', "//label[.='$field']");
498
			if($label && $for = $label->getAttribute('for')) {
499
				$formField = $this->getSession()->getPage()->find('xpath', "//*[@id='$for']");
500
				if($formField) $formFields[] = $formField;
501
			}
502
		}
503
504
		assertGreaterThan(0, count($formFields), sprintf(
505
			'Chosen.js dropdown named "%s" not found',
506
			$field
507
		));
508
509
		// Traverse up to field holder
510
		$container = null;
511
		foreach($formFields as $formField) {
512
			$container = $this->findParentByClass($formField, 'field');
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $container is correct as $this->findParentByClass($formField, 'field') (which targets SilverStripe\Framework\T...xt::findParentByClass()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
513
			if($container) break; // Default to first visible container
514
		}
515
516
		assertNotNull($container, 'Chosen.js field container not found');
517
518
		// Click on newly expanded list element, indirectly setting the dropdown value
519
		$linkEl = $container->find('xpath', './/a[./@href]');
0 ignored issues
show
Bug introduced by
The method find cannot be called on $container (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
520
		assertNotNull($linkEl, 'Chosen.js link element not found');
521
		$this->getSession()->wait(100); // wait for dropdown overlay to appear
522
		$linkEl->click();
523
524
		if(in_array('treedropdown', explode(' ', $container->getAttribute('class')))) {
0 ignored issues
show
Bug introduced by
The method getAttribute cannot be called on $container (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
525
			// wait for ajax dropdown to load
526
			$this->getSession()->wait(
527
				5000,
528
				"window.jQuery && "
529
				. "window.jQuery('#" . $container->getAttribute('id') . " .treedropdownfield-panel li').length > 0"
0 ignored issues
show
Bug introduced by
The method getAttribute cannot be called on $container (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
530
			);
531
		} else {
532
			// wait for dropdown overlay to appear (might be animated)
533
			$this->getSession()->wait(300);
534
		}
535
536
		$listEl = $container->find('xpath', sprintf('.//li[contains(normalize-space(string(.)), \'%s\')]', $value));
0 ignored issues
show
Bug introduced by
The method find cannot be called on $container (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
537
		if(null === $listEl) {
538
			throw new \InvalidArgumentException(sprintf(
539
				'Chosen.js list element with title "%s" not found',
540
				$value
541
			));
542
		}
543
544
		$listLinkEl = $listEl->find('xpath', './/a');
545
		if($listLinkEl) {
546
			$listLinkEl->click();
547
		} else {
548
			$listEl->click();
549
		}
550
	}
551
552
	/**
553
	 * Returns fixed step argument (with \\" replaced back to ").
554
	 *
555
	 * @param string $argument
556
	 *
557
	 * @return string
558
	 */
559
	protected function fixStepArgument($argument) {
560
		return str_replace('\\"', '"', $argument);
561
	}
562
563
	/**
564
	 * Returns the closest parent element having a specific class attribute.
565
	 *
566
	 * @param  NodeElement $el
567
	 * @param  String  $class
568
	 * @return Element|null
569
	 */
570
	protected function findParentByClass(NodeElement $el, $class) {
571
		$container = $el->getParent();
572
		while($container && $container->getTagName() != 'body') {
573
			if($container->isVisible() && in_array($class, explode(' ', $container->getAttribute('class')))) {
574
				return $container;
575
			}
576
			$container = $container->getParent();
577
		}
578
579
		return null;
580
	}
581
}
582