Completed
Push — master ( 96bcde...598ec2 )
by Paweł
29:21 queued 29:07
created

UpdateSimpleProductPage::getDefinedElements()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
cc 1
eloc 20
nc 1
nop 0
1
<?php
2
3
/*
4
 * This file is part of the Sylius package.
5
 *
6
 * (c) Paweł Jędrzejewski
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Sylius\Behat\Page\Admin\Product;
13
14
use Behat\Mink\Driver\Selenium2Driver;
15
use Behat\Mink\Element\NodeElement;
16
use Sylius\Behat\Behaviour\ChecksCodeImmutability;
17
use Sylius\Behat\Page\Admin\Crud\UpdatePage as BaseUpdatePage;
18
use Sylius\Component\Core\Model\ChannelInterface;
19
use Sylius\Component\Core\Model\TaxonInterface;
20
use Sylius\Component\Currency\Model\CurrencyInterface;
21
use Sylius\Component\Product\Model\ProductAssociationTypeInterface;
22
use Webmozart\Assert\Assert;
23
24
/**
25
 * @author Łukasz Chruściel <[email protected]>
26
 */
27
class UpdateSimpleProductPage extends BaseUpdatePage implements UpdateSimpleProductPageInterface
28
{
29
    use ChecksCodeImmutability;
30
31
    /**
32
     * {@inheritdoc}
33
     */
34
    public function nameItIn($name, $localeCode)
35
    {
36
        $this->activateLanguageTab($localeCode);
37
        $this->getElement('name', ['%locale%' => $localeCode])->setValue($name);
38
39
        $this->waitForSlugGenerationIfNecessary($localeCode);
40
    }
41
42
    /**
43
     * {@inheritdoc}
44
     */
45
    public function specifyPrice($channelName, $price)
46
    {
47
        $this->getElement('price', ['%channel%' => $channelName])->setValue($price);
48
    }
49
50
    public function addSelectedAttributes()
51
    {
52
        $this->clickTabIfItsNotActive('attributes');
53
        $this->getDocument()->pressButton('Add attributes');
54
55
        $form = $this->getDocument()->find('css', 'form');
56
57
        $this->getDocument()->waitFor(1, function () use ($form) {
58
            return $form->hasClass('loading');
59
        });
60
    }
61
62
    /**
63
     * {@inheritdoc}
64
     */
65
    public function removeAttribute($attributeName, $localeCode)
66
    {
67
        $this->clickTabIfItsNotActive('attributes');
68
69
        $this->getElement('attribute_delete_button', ['%attributeName%' => $attributeName, '$localeCode%' => $localeCode])->press();
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75
    public function getAttributeValue($attribute, $localeCode)
76
    {
77
        $this->clickTabIfItsNotActive('attributes');
78
        $this->clickLocaleTabIfItsNotActive($localeCode);
79
80
        return $this->getElement('attribute', ['%attributeName%' => $attribute, '%localeCode%' => $localeCode])->getValue();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getElement...caleCode))->getValue(); (string|boolean|array) is incompatible with the return type declared by the interface Sylius\Behat\Page\Admin\...face::getAttributeValue of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86
    public function getNumberOfAttributes()
87
    {
88
        return count($this->getDocument()->findAll('css', '.attribute'));
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94
    public function hasAttribute($attributeName)
95
    {
96
        return null !== $this->getDocument()->find('css', sprintf('.attribute .label:contains("%s")', $attributeName));
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function selectMainTaxon(TaxonInterface $taxon)
103
    {
104
        $this->openTaxonBookmarks();
105
106
        Assert::isInstanceOf($this->getDriver(), Selenium2Driver::class);
107
108
        $mainTaxonElement = $this->getElement('main_taxon')->getParent();
109
110
        $isVisibleScript = sprintf(
111
            '$(document.evaluate("%s", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).dropdown("is visible")',
112
            $mainTaxonElement->getXpath()
113
        );
114
        $isAnyAsyncActionInProgressScript = sprintf(
115
            'jQuery.active'
116
        );
117
118
        $this->getDocument()->waitFor(5, function () use ($isAnyAsyncActionInProgressScript) {
119
            return !(bool) $this->getDriver()->evaluateScript($isAnyAsyncActionInProgressScript);
120
        });
121
122
        $mainTaxonElement->click();
123
124
        $this->getDocument()->waitFor(5, function () use ($isAnyAsyncActionInProgressScript) {
125
            return !(bool) $this->getDriver()->evaluateScript($isAnyAsyncActionInProgressScript);
126
        });
127
128
        $this->getDocument()->waitFor(5, function () use ($isVisibleScript) {
129
            return $this->getDriver()->evaluateScript($isVisibleScript);
130
        });
131
132
        $mainTaxonItemElement = $mainTaxonElement->find('css', sprintf('div.item:contains("%s")', $taxon->getName()));
133
134
        $mainTaxonItemElement->click();
135
136
        $this->getDocument()->waitFor(5, function () use ($isVisibleScript) {
137
            return !$this->getDriver()->evaluateScript($isVisibleScript);
138
        });
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144
    public function isMainTaxonChosen($taxonName)
145
    {
146
        $this->openTaxonBookmarks();
147
148
        return $taxonName === $this->getDocument()->find('css', '.search > .text')->getText();
149
    }
150
151
    public function disableTracking()
152
    {
153
        $this->getElement('tracked')->uncheck();
154
    }
155
156
    public function enableTracking()
157
    {
158
        $this->getElement('tracked')->check();
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164
    public function isTracked()
165
    {
166
        return $this->getElement('tracked')->isChecked();
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function enableSlugModification($locale)
173
    {
174
        $this->getElement('toggle_slug_modification_button', ['%locale%' => $locale])->press();
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180
    public function isImageWithTypeDisplayed($type)
181
    {
182
        $imageElement = $this->getImageElementByType($type);
183
184
        if (null === $imageElement) {
185
            return false;
186
        }
187
188
        $imageUrl = $imageElement->find('css', 'img')->getAttribute('src');
189
        $this->getDriver()->visit($imageUrl);
190
        $pageText = $this->getDocument()->getText();
191
        $this->getDriver()->back();
192
193
        return false === stripos($pageText, '404 Not Found');
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199
    public function attachImage($path, $type = null)
200
    {
201
        $this->clickTabIfItsNotActive('media');
202
203
        $filesPath = $this->getParameter('files_path');
204
205
        $this->getDocument()->clickLink('Add');
206
207
        $imageForm = $this->getLastImageElement();
208
        if (null !== $type) {
209
            $imageForm->fillField('Type', $type);
210
        }
211
212
        $imageForm->find('css', 'input[type="file"]')->attachFile($filesPath.$path);
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218
    public function changeImageWithType($type, $path)
219
    {
220
        $filesPath = $this->getParameter('files_path');
221
222
        $imageForm = $this->getImageElementByType($type);
223
        $imageForm->find('css', 'input[type="file"]')->attachFile($filesPath.$path);
224
    }
225
226
    /**
227
     * {@inheritdoc}
228
     */
229
    public function removeImageWithType($type)
230
    {
231
        $this->clickTabIfItsNotActive('media');
232
233
        $imageElement = $this->getImageElementByType($type);
234
        $imageElement->clickLink('Delete');
235
    }
236
237
    public function removeFirstImage()
238
    {
239
        $this->clickTabIfItsNotActive('media');
240
241
        $imageElement = $this->getFirstImageElement();
242
        $imageElement->clickLink('Delete');
243
    }
244
245
    /**
246
     * {@inheritdoc}
247
     */
248
    public function modifyFirstImageType($type)
249
    {
250
        $this->clickTabIfItsNotActive('media');
251
252
        $firstImage = $this->getFirstImageElement();
253
        $this->setImageType($firstImage, $type);
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function countImages()
260
    {
261
        $imageElements = $this->getImageElements();
262
263
        return count($imageElements);
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269
    public function isSlugReadOnlyIn($locale)
270
    {
271
        return 'readonly' === $this->getElement('slug', ['%locale%' => $locale])->getAttribute('readonly');
272
    }
273
274
    /**
275
     * {@inheritdoc}
276
     */
277
    public function associateProducts(ProductAssociationTypeInterface $productAssociationType, array $productsNames)
278
    {
279
        $this->clickTab('associations');
280
281
        Assert::isInstanceOf($this->getDriver(), Selenium2Driver::class);
282
283
        $dropdown = $this->getElement('association_dropdown', [
284
            '%association%' => $productAssociationType->getName()
285
        ]);
286
        $dropdown->click();
287
288
        foreach ($productsNames as $productName) {
289
            $dropdown->waitFor(5, function () use ($productName, $productAssociationType) {
290
                return $this->hasElement('association_dropdown_item', [
291
                    '%association%' => $productAssociationType->getName(),
292
                    '%item%' => $productName,
293
                ]);
294
            });
295
296
            $item = $this->getElement('association_dropdown_item', [
297
                '%association%' => $productAssociationType->getName(),
298
                '%item%' => $productName,
299
            ]);
300
            $item->click();
301
        }
302
    }
303
304
    /**
305
     * {@inheritdoc}
306
     */
307
    public function hasAssociatedProduct($productName, ProductAssociationTypeInterface $productAssociationType)
308
    {
309
        $this->clickTabIfItsNotActive('associations');
310
311
        return $this->hasElement('association_dropdown_item', [
312
            '%association%' => $productAssociationType->getName(),
313
            '%item%' => $productName,
314
        ]);
315
    }
316
317
    /**
318
     * {@inheritdoc}
319
     */
320
    public function removeAssociatedProduct($productName, ProductAssociationTypeInterface $productAssociationType)
321
    {
322
        $this->clickTabIfItsNotActive('associations');
323
324
        $item = $this->getElement('association_dropdown_item_selected', [
325
            '%association%' => $productAssociationType->getName(),
326
            '%item%' => $productName,
327
        ]);
328
329
        $deleteIcon = $item->find('css', 'i.delete');
330
        Assert::notNull($deleteIcon);
331
        $deleteIcon->click();
332
    }
333
334
    /**
335
     * {@inheritdoc}
336
     */
337
    public function getPricingConfigurationForChannelAndCurrencyCalculator(ChannelInterface $channel, CurrencyInterface $currency)
338
    {
339
        $priceConfigurationElement = $this->getElement('pricing_configuration');
340
        $priceElement = $priceConfigurationElement
341
            ->find('css', sprintf('label:contains("%s %s")', $channel->getCode(), $currency->getCode()))->getParent();
342
343
        return $priceElement->find('css', 'input')->getValue();
344
    }
345
346
    /**
347
     * {@inheritdoc}
348
     */
349
    public function getSlug($locale)
350
    {
351
        $this->activateLanguageTab($locale);
352
353
        return $this->getElement('slug', ['%locale%' => $locale])->getValue();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getElement... $locale))->getValue(); (string|boolean|array) is incompatible with the return type declared by the interface Sylius\Behat\Page\Admin\...tPageInterface::getSlug of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
354
    }
355
356
    /**
357
     * {@inheritdoc}
358
     */
359
    public function specifySlugIn($slug, $locale)
360
    {
361
        $this->activateLanguageTab($locale);
362
363
        $this->getElement('slug', ['%locale%' => $locale])->setValue($slug);
364
    }
365
366
    /**
367
     * {@inheritdoc}
368
     */
369
    public function activateLanguageTab($locale)
370
    {
371
        if (!$this->getDriver() instanceof Selenium2Driver) {
372
            return;
373
        }
374
375
        $languageTabTitle = $this->getElement('language_tab', ['%locale%' => $locale]);
376
        if (!$languageTabTitle->hasClass('active')) {
377
            $languageTabTitle->click();
378
        }
379
    }
380
381
    public function getPriceForChannel($channelName)
382
    {
383
        return $this->getElement('price', ['%channel%' => $channelName])->getValue();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getElement...nnelName))->getValue(); (string|boolean|array) is incompatible with the return type declared by the interface Sylius\Behat\Page\Admin\...ace::getPriceForChannel of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
384
    }
385
386
    /**
387
     * {@inheritdoc}
388
     */
389
    protected function getCodeElement()
390
    {
391
        return $this->getElement('code');
392
    }
393
394
    /**
395
     * {@inheritdoc}
396
     */
397
    protected function getElement($name, array $parameters = [])
398
    {
399
        if (!isset($parameters['%locale%'])) {
400
            $parameters['%locale%'] = 'en_US';
401
        }
402
403
        return parent::getElement($name, $parameters);
404
    }
405
406
    /**
407
     * {@inheritdoc}
408
     */
409
    protected function getDefinedElements()
410
    {
411
        return array_merge(parent::getDefinedElements(), [
412
            'association_dropdown' => '.field > label:contains("%association%") ~ .product-select',
413
            'association_dropdown_item' => '.field > label:contains("%association%") ~ .product-select > div.menu > div.item:contains("%item%")',
414
            'association_dropdown_item_selected' => '.field > label:contains("%association%") ~ .product-select > a.label:contains("%item%")',
415
            'attribute' => '.tab[data-tab="%localeCode%"] .attribute .label:contains("%attributeName%") ~ input',
416
            'attribute_delete_button' => '.tab[data-tab="%localeCode%"] .attribute .label:contains("%attributeName%") ~ button',
417
            'code' => '#sylius_product_code',
418
            'images' => '#sylius_product_images',
419
            'language_tab' => '[data-locale="%locale%"] .title',
420
            'locale_tab' => '#attributesContainer .menu [data-tab="%localeCode%"]',
421
            'name' => '#sylius_product_translations_%locale%_name',
422
            'price' => '#sylius_product_variant_channelPricings [data-form-collection="item"]:contains("%channel%") input',
423
            'pricing_configuration' => '#sylius_calculator_container',
424
            'main_taxon' => '#sylius_product_mainTaxon',
425
            'slug' => '#sylius_product_translations_%locale%_slug',
426
            'tab' => '.menu [data-tab="%name%"]',
427
            'taxonomy' => 'a[data-tab="taxonomy"]',
428
            'tracked' => '#sylius_product_variant_tracked',
429
            'toggle_slug_modification_button' => '[data-locale="%locale%"] .toggle-product-slug-modification',
430
        ]);
431
    }
432
433
    private function openTaxonBookmarks()
434
    {
435
        $this->getElement('taxonomy')->click();
436
    }
437
438
    /**
439
     * @param string $tabName
440
     */
441
    private function clickTabIfItsNotActive($tabName)
442
    {
443
        $attributesTab = $this->getElement('tab', ['%name%' => $tabName]);
444
        if (!$attributesTab->hasClass('active')) {
445
            $attributesTab->click();
446
        }
447
    }
448
449
    /**
450
     * @param string $tabName
451
     */
452
    private function clickTab($tabName)
453
    {
454
        $attributesTab = $this->getElement('tab', ['%name%' => $tabName]);
455
        $attributesTab->click();
456
    }
457
458
    /**
459
     * @param string $localeCode
460
     */
461
    private function clickLocaleTabIfItsNotActive($localeCode)
462
    {
463
        $localeTab = $this->getElement('locale_tab', ['%localeCode%' => $localeCode]);
464
        if (!$localeTab->hasClass('active')) {
465
            $localeTab->click();
466
        }
467
    }
468
469
    /**
470
     * @param string $type
471
     *
472
     * @return NodeElement
473
     */
474
    private function getImageElementByType($type)
475
    {
476
        $images = $this->getElement('images');
477
        $typeInput = $images->find('css', 'input[value="'.$type.'"]');
478
479
        if (null === $typeInput) {
480
            return null;
481
        }
482
483
        return $typeInput->getParent()->getParent()->getParent();
484
    }
485
486
    /**
487
     * @return NodeElement[]
488
     */
489
    private function getImageElements()
490
    {
491
        $images = $this->getElement('images');
492
493
        return $images->findAll('css', 'div[data-form-collection="item"]');
494
    }
495
496
    /**
497
     * @return NodeElement
498
     */
499
    private function getLastImageElement()
500
    {
501
        $imageElements = $this->getImageElements();
502
503
        Assert::notEmpty($imageElements);
504
505
        return end($imageElements);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression end($imageElements); of type Behat\Mink\Element\NodeElement|false adds false to the return on line 505 which is incompatible with the return type documented by Sylius\Behat\Page\Admin\...ge::getLastImageElement of type Behat\Mink\Element\NodeElement. It seems like you forgot to handle an error condition.
Loading history...
506
    }
507
508
    /**
509
     * @return NodeElement
510
     */
511
    private function getFirstImageElement()
512
    {
513
        $imageElements = $this->getImageElements();
514
515
        Assert::notEmpty($imageElements);
516
517
        return reset($imageElements);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression reset($imageElements); of type Behat\Mink\Element\NodeElement|false adds false to the return on line 517 which is incompatible with the return type documented by Sylius\Behat\Page\Admin\...e::getFirstImageElement of type Behat\Mink\Element\NodeElement. It seems like you forgot to handle an error condition.
Loading history...
518
    }
519
520
    /**
521
     * @param string $locale
522
     */
523
    private function waitForSlugGenerationIfNecessary($locale)
524
    {
525
        if (!$this->getDriver() instanceof Selenium2Driver) {
526
            return;
527
        }
528
529
        $slugElement = $this->getElement('slug', ['%locale%' => $locale]);
530
        if ($slugElement->hasAttribute('readonly')) {
531
            return;
532
        }
533
534
        $value = $slugElement->getValue();
535
        $this->getDocument()->waitFor(10, function () use ($slugElement, $value) {
536
            return $value !== $slugElement->getValue();
537
        });
538
    }
539
540
    /**
541
     * @param NodeElement $imageElement
542
     * @param string $type
543
     */
544
    private function setImageType(NodeElement $imageElement, $type)
545
    {
546
        $typeField = $imageElement->findField('Type');
547
        $typeField->setValue($type);
548
    }
549
}
550