Completed
Push — master ( 799620...8a5869 )
by Kamil
116:02 queued 102:30
created

Page/Admin/Product/UpdateSimpleProductPage.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
declare(strict_types=1);
13
14
namespace Sylius\Behat\Page\Admin\Product;
15
16
use Behat\Mink\Driver\Selenium2Driver;
17
use Behat\Mink\Element\NodeElement;
18
use Sylius\Behat\Behaviour\ChecksCodeImmutability;
19
use Sylius\Behat\Page\Admin\Crud\UpdatePage as BaseUpdatePage;
20
use Sylius\Behat\Service\AutocompleteHelper;
21
use Sylius\Behat\Service\SlugGenerationHelper;
22
use Sylius\Component\Core\Model\ChannelInterface;
23
use Sylius\Component\Core\Model\TaxonInterface;
24
use Sylius\Component\Currency\Model\CurrencyInterface;
25
use Sylius\Component\Product\Model\ProductAssociationTypeInterface;
26
use Webmozart\Assert\Assert;
27
28
class UpdateSimpleProductPage extends BaseUpdatePage implements UpdateSimpleProductPageInterface
29
{
30
    use ChecksCodeImmutability;
31
32
    /** @var array */
33
    private $imageUrls = [];
34
35
    /**
36
     * {@inheritdoc}
37
     */
38
    public function nameItIn($name, $localeCode)
39
    {
40
        $this->activateLanguageTab($localeCode);
41
        $this->getElement('name', ['%locale%' => $localeCode])->setValue($name);
42
43
        if ($this->getDriver() instanceof Selenium2Driver) {
44
            SlugGenerationHelper::waitForSlugGeneration(
45
                $this->getSession(),
46
                $this->getElement('slug', ['%locale%' => $localeCode])
47
            );
48
        }
49
    }
50
51
    /**
52
     * {@inheritdoc}
53
     */
54
    public function specifyPrice($channelName, $price)
55
    {
56
        $this->getElement('price', ['%channelName%' => $channelName])->setValue($price);
57
    }
58
59
    /**
60
     * {@inheritdoc}
61
     */
62
    public function specifyOriginalPrice($channelName, $originalPrice)
63
    {
64
        $this->getElement('original_price', ['%channelName%' => $channelName])->setValue($originalPrice);
65
    }
66
67
    public function addSelectedAttributes()
68
    {
69
        $this->clickTabIfItsNotActive('attributes');
70
        $this->getDocument()->pressButton('Add attributes');
71
72
        $form = $this->getDocument()->find('css', 'form');
73
74
        $this->getDocument()->waitFor(1, function () use ($form) {
75
            return $form->hasClass('loading');
76
        });
77
    }
78
79
    /**
80
     * {@inheritdoc}
81
     */
82
    public function removeAttribute($attributeName, $localeCode)
83
    {
84
        $this->clickTabIfItsNotActive('attributes');
85
86
        $this->getElement('attribute_delete_button', ['%attributeName%' => $attributeName, '$localeCode%' => $localeCode])->press();
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92
    public function getAttributeValue($attribute, $localeCode)
93
    {
94
        $this->clickTabIfItsNotActive('attributes');
95
        $this->clickLocaleTabIfItsNotActive($localeCode);
96
97
        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...
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    public function getAttributeValidationErrors($attributeName, $localeCode)
104
    {
105
        $this->clickTabIfItsNotActive('attributes');
106
        $this->clickLocaleTabIfItsNotActive($localeCode);
107
108
        $validationError = $this->getElement('attribute_element')->find('css', '.sylius-validation-error');
109
110
        return $validationError->getText();
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116
    public function getNumberOfAttributes()
117
    {
118
        return count($this->getDocument()->findAll('css', '.attribute'));
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124
    public function hasAttribute($attributeName)
125
    {
126
        return null !== $this->getDocument()->find('css', sprintf('.attribute .label:contains("%s")', $attributeName));
127
    }
128
129
    /**
130
     * {@inheritdoc}
131
     */
132
    public function selectMainTaxon(TaxonInterface $taxon)
133
    {
134
        $this->openTaxonBookmarks();
135
136
        $mainTaxonElement = $this->getElement('main_taxon')->getParent();
137
138
        AutocompleteHelper::chooseValue($this->getSession(), $mainTaxonElement, $taxon->getName());
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
        SlugGenerationHelper::enableSlugModification(
175
            $this->getSession(),
176
            $this->getElement('toggle_slug_modification_button', ['%locale%' => $locale])
177
        );
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183
    public function isImageWithTypeDisplayed($type)
184
    {
185
        $imageElement = $this->getImageElementByType($type);
186
187
        $imageUrl = $imageElement ? $imageElement->find('css', 'img')->getAttribute('src') : $this->provideImageUrlForType($type);
188
        if (null === $imageElement && null === $imageUrl) {
189
            return false;
190
        }
191
192
        $this->getDriver()->visit($imageUrl);
193
        $pageText = $this->getDocument()->getText();
194
        $this->getDriver()->back();
195
196
        return false === stripos($pageText, '404 Not Found');
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202
    public function attachImage($path, $type = null)
203
    {
204
        $this->clickTabIfItsNotActive('media');
205
206
        $filesPath = $this->getParameter('files_path');
207
208
        $this->getDocument()->clickLink('Add');
209
210
        $imageForm = $this->getLastImageElement();
211
        if (null !== $type) {
212
            $imageForm->fillField('Type', $type);
213
        }
214
215
        $imageForm->find('css', 'input[type="file"]')->attachFile($filesPath . $path);
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221
    public function changeImageWithType($type, $path)
222
    {
223
        $filesPath = $this->getParameter('files_path');
224
225
        $imageForm = $this->getImageElementByType($type);
226
        $imageForm->find('css', 'input[type="file"]')->attachFile($filesPath . $path);
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     */
232
    public function removeImageWithType($type)
233
    {
234
        $this->clickTabIfItsNotActive('media');
235
236
        $imageElement = $this->getImageElementByType($type);
237
        $imageSourceElement = $imageElement->find('css', 'img');
238
        if (null !== $imageSourceElement) {
239
            $this->saveImageUrlForType($type, $imageSourceElement->getAttribute('src'));
240
        }
241
242
        $imageElement->clickLink('Delete');
243
    }
244
245
    public function removeFirstImage()
246
    {
247
        $this->clickTabIfItsNotActive('media');
248
        $imageElement = $this->getFirstImageElement();
249
        $imageTypeElement = $imageElement->find('css', 'input[type=text]');
250
        $imageSourceElement = $imageElement->find('css', 'img');
251
252
        if (null !== $imageTypeElement && null !== $imageSourceElement) {
253
            $this->saveImageUrlForType(
254
                $imageTypeElement->getValue(),
255
                $imageSourceElement->getAttribute('src')
256
            );
257
        }
258
        $imageElement->clickLink('Delete');
259
    }
260
261
    /**
262
     * {@inheritdoc}
263
     */
264
    public function modifyFirstImageType($type)
265
    {
266
        $this->clickTabIfItsNotActive('media');
267
268
        $firstImage = $this->getFirstImageElement();
269
        $this->setImageType($firstImage, $type);
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    public function countImages()
276
    {
277
        $imageElements = $this->getImageElements();
278
279
        return count($imageElements);
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285
    public function isSlugReadonlyIn($locale)
286
    {
287
        return SlugGenerationHelper::isSlugReadonly(
288
            $this->getSession(),
289
            $this->getElement('slug', ['%locale%' => $locale])
290
        );
291
    }
292
293
    /**
294
     * {@inheritdoc}
295
     */
296
    public function associateProducts(ProductAssociationTypeInterface $productAssociationType, array $productsNames)
297
    {
298
        $this->clickTab('associations');
299
300
        Assert::isInstanceOf($this->getDriver(), Selenium2Driver::class);
301
302
        $dropdown = $this->getElement('association_dropdown', [
303
            '%association%' => $productAssociationType->getName(),
304
        ]);
305
        $dropdown->click();
306
307
        foreach ($productsNames as $productName) {
308
            $dropdown->waitFor(5, function () use ($productName, $productAssociationType) {
309
                return $this->hasElement('association_dropdown_item', [
310
                    '%association%' => $productAssociationType->getName(),
311
                    '%item%' => $productName,
312
                ]);
313
            });
314
315
            $item = $this->getElement('association_dropdown_item', [
316
                '%association%' => $productAssociationType->getName(),
317
                '%item%' => $productName,
318
            ]);
319
            $item->click();
320
        }
321
    }
322
323
    /**
324
     * {@inheritdoc}
325
     */
326
    public function hasAssociatedProduct($productName, ProductAssociationTypeInterface $productAssociationType)
327
    {
328
        $this->clickTabIfItsNotActive('associations');
329
330
        return $this->hasElement('association_dropdown_item', [
331
            '%association%' => $productAssociationType->getName(),
332
            '%item%' => $productName,
333
        ]);
334
    }
335
336
    /**
337
     * {@inheritdoc}
338
     */
339
    public function removeAssociatedProduct($productName, ProductAssociationTypeInterface $productAssociationType)
340
    {
341
        $this->clickTabIfItsNotActive('associations');
342
343
        $item = $this->getElement('association_dropdown_item_selected', [
344
            '%association%' => $productAssociationType->getName(),
345
            '%item%' => $productName,
346
        ]);
347
348
        $deleteIcon = $item->find('css', 'i.delete');
349
        Assert::notNull($deleteIcon);
350
        $deleteIcon->click();
351
    }
352
353
    /**
354
     * {@inheritdoc}
355
     */
356
    public function getPricingConfigurationForChannelAndCurrencyCalculator(ChannelInterface $channel, CurrencyInterface $currency)
357
    {
358
        $priceConfigurationElement = $this->getElement('pricing_configuration');
359
        $priceElement = $priceConfigurationElement
360
            ->find('css', sprintf('label:contains("%s %s")', $channel->getCode(), $currency->getCode()))->getParent();
361
362
        return $priceElement->find('css', 'input')->getValue();
363
    }
364
365
    /**
366
     * {@inheritdoc}
367
     */
368
    public function getSlug($locale)
369
    {
370
        $this->activateLanguageTab($locale);
371
372
        return $this->getElement('slug', ['%locale%' => $locale])->getValue();
373
    }
374
375
    /**
376
     * {@inheritdoc}
377
     */
378
    public function specifySlugIn($slug, $locale)
379
    {
380
        $this->activateLanguageTab($locale);
381
382
        $this->getElement('slug', ['%locale%' => $locale])->setValue($slug);
383
    }
384
385
    /**
386
     * {@inheritdoc}
387
     */
388
    public function activateLanguageTab($locale)
389
    {
390
        if (!$this->getDriver() instanceof Selenium2Driver) {
391
            return;
392
        }
393
394
        $languageTabTitle = $this->getElement('language_tab', ['%locale%' => $locale]);
395
        if (!$languageTabTitle->hasClass('active')) {
396
            $languageTabTitle->click();
397
        }
398
    }
399
400
    public function getPriceForChannel($channelName)
401
    {
402
        return $this->getElement('price', ['%channelName%' => $channelName])->getValue();
403
    }
404
405
    /**
406
     * {@inheritdoc}
407
     */
408
    public function getOriginalPriceForChannel($channelName)
409
    {
410
        return $this->getElement('original_price', ['%channelName%' => $channelName])->getValue();
411
    }
412
413
    /**
414
     * {@inheritdoc}
415
     */
416
    public function isShippingRequired()
417
    {
418
        return $this->getElement('shipping_required')->isChecked();
419
    }
420
421
    /**
422
     * {@inheritdoc}
423
     */
424
    protected function getCodeElement()
425
    {
426
        return $this->getElement('code');
427
    }
428
429
    /**
430
     * {@inheritdoc}
431
     */
432
    protected function getElement($name, array $parameters = [])
433
    {
434
        if (!isset($parameters['%locale%'])) {
435
            $parameters['%locale%'] = 'en_US';
436
        }
437
438
        return parent::getElement($name, $parameters);
439
    }
440
441
    /**
442
     * {@inheritdoc}
443
     */
444
    protected function getDefinedElements()
445
    {
446
        return array_merge(parent::getDefinedElements(), [
447
            'association_dropdown' => '.field > label:contains("%association%") ~ .product-select',
448
            'association_dropdown_item' => '.field > label:contains("%association%") ~ .product-select > div.menu > div.item:contains("%item%")',
449
            'association_dropdown_item_selected' => '.field > label:contains("%association%") ~ .product-select > a.label:contains("%item%")',
450
            'attribute' => '.tab[data-tab="%localeCode%"] .attribute:contains("%attributeName%") input',
451
            'attribute_element' => '.attribute',
452
            'attribute_delete_button' => '.tab[data-tab="%localeCode%"] .attribute .label:contains("%attributeName%") ~ button',
453
            'code' => '#sylius_product_code',
454
            'images' => '#sylius_product_images',
455
            'language_tab' => '[data-locale="%locale%"] .title',
456
            'locale_tab' => '#attributesContainer .menu [data-tab="%localeCode%"]',
457
            'name' => '#sylius_product_translations_%locale%_name',
458
            'original_price' => '#sylius_product_variant_channelPricings > .field:contains("%channelName%") input[name$="[originalPrice]"]',
459
            'price' => '#sylius_product_variant_channelPricings > .field:contains("%channelName%") input[name$="[price]"]',
460
            'pricing_configuration' => '#sylius_calculator_container',
461
            'main_taxon' => '#sylius_product_mainTaxon',
462
            'shipping_required' => '#sylius_product_variant_shippingRequired',
463
            'slug' => '#sylius_product_translations_%locale%_slug',
464
            'tab' => '.menu [data-tab="%name%"]',
465
            'taxonomy' => 'a[data-tab="taxonomy"]',
466
            'tracked' => '#sylius_product_variant_tracked',
467
            'toggle_slug_modification_button' => '[data-locale="%locale%"] .toggle-product-slug-modification',
468
        ]);
469
    }
470
471
    private function openTaxonBookmarks()
472
    {
473
        $this->getElement('taxonomy')->click();
474
    }
475
476
    /**
477
     * @param string $tabName
478
     */
479
    private function clickTabIfItsNotActive($tabName)
480
    {
481
        $attributesTab = $this->getElement('tab', ['%name%' => $tabName]);
482
        if (!$attributesTab->hasClass('active')) {
483
            $attributesTab->click();
484
        }
485
    }
486
487
    /**
488
     * @param string $tabName
489
     */
490
    private function clickTab($tabName)
491
    {
492
        $attributesTab = $this->getElement('tab', ['%name%' => $tabName]);
493
        $attributesTab->click();
494
    }
495
496
    /**
497
     * @param string $localeCode
498
     */
499
    private function clickLocaleTabIfItsNotActive($localeCode)
500
    {
501
        $localeTab = $this->getElement('locale_tab', ['%localeCode%' => $localeCode]);
502
        if (!$localeTab->hasClass('active')) {
503
            $localeTab->click();
504
        }
505
    }
506
507
    /**
508
     * @param string $type
509
     *
510
     * @return NodeElement
511
     */
512
    private function getImageElementByType($type)
513
    {
514
        $images = $this->getElement('images');
515
        $typeInput = $images->find('css', 'input[value="' . $type . '"]');
516
517
        if (null === $typeInput) {
518
            return null;
519
        }
520
521
        return $typeInput->getParent()->getParent()->getParent();
522
    }
523
524
    /**
525
     * @return NodeElement[]
526
     */
527
    private function getImageElements()
528
    {
529
        $images = $this->getElement('images');
530
531
        return $images->findAll('css', 'div[data-form-collection="item"]');
532
    }
533
534
    /**
535
     * @return NodeElement
536
     */
537
    private function getLastImageElement()
538
    {
539
        $imageElements = $this->getImageElements();
540
541
        Assert::notEmpty($imageElements);
542
543
        return end($imageElements);
544
    }
545
546
    /**
547
     * @return NodeElement
548
     */
549
    private function getFirstImageElement()
550
    {
551
        $imageElements = $this->getImageElements();
552
553
        Assert::notEmpty($imageElements);
554
555
        return reset($imageElements);
556
    }
557
558
    /**
559
     * @param NodeElement $imageElement
560
     * @param string $type
561
     */
562
    private function setImageType(NodeElement $imageElement, $type)
563
    {
564
        $typeField = $imageElement->findField('Type');
565
        $typeField->setValue($type);
566
    }
567
568
    private function provideImageUrlForType(string $type): ?string
569
    {
570
        return $this->imageUrls[$type] ?? null;
571
    }
572
573
    private function saveImageUrlForType(string $type, string $imageUrl): void
574
    {
575
        if (false !== strpos($imageUrl, 'data:image/jpeg')) {
576
            return;
577
        }
578
579
        $this->imageUrls[$type] = $imageUrl;
580
    }
581
}
582