Completed
Pull Request — master (#5408)
by Damian
23:40 queued 12:41
created

CmsUiContext::handleCmsLoadingAfterStep()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

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