Passed
Push — feature/tests ( a58f99...9b7685 )
by Marc
02:09
created

criticalCssShouldNotExistInHead()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace MOrtola\BehatSEOContexts\Context;
4
5
use Behat\Mink\Element\NodeElement;
6
use Behat\Mink\Exception\UnsupportedDriverActionException;
7
use PHPUnit\Framework\Assert;
8
9
class PerformanceContext extends BaseContext
10
{
11
    const RESOURCE_TYPES = [
12
        'PNG' => 'png',
13
        'HTML' => 'html',
14
        'JPEG' => 'jpeg',
15
        'GIF' => 'gif',
16
        'ICO' => 'ico',
17
        'JAVASCRIPT' => 'js',
18
        'CSS' => 'css',
19
        'CSS_INLINE_HEAD' => 'css-inline-head',
20
        'CSS_LINK_HEAD' => 'css-link-head',
21
    ];
22
23
    /**
24
     * @throws \Exception
25
     *
26
     * @Then /^Javascript code should load (async|defer)$/
27
     */
28
    public function javascriptFilesShouldLoadAsync()
29
    {
30
        $scriptElements = $this->getPageResources(self::RESOURCE_TYPES['JAVASCRIPT']);
31
32
        foreach ($scriptElements as $scriptElement) {
33
            Assert::assertTrue(
34
                $scriptElement->hasAttribute('async') || $scriptElement->hasAttribute('defer'),
35
                sprintf(
36
                    'Javascript file %s is render blocking in %s',
37
                    $this->getResourceUrl($scriptElement, self::RESOURCE_TYPES['JAVASCRIPT']),
38
                    $this->getCurrentUrl()
39
                )
40
            );
41
        }
42
    }
43
44
    /**
45
     * @return NodeElement[]
46
     * @throws \Exception
47
     */
48
    private function getPageResources(string $resourceType, bool $selfHosted = true, bool $expected = true): array
49
    {
50
        switch ($resourceType) {
51
            case self::RESOURCE_TYPES['JPEG']:
52
                $xpath = '//img[contains(@src,".jpeg")]';
53
54
                break;
55
            case self::RESOURCE_TYPES['PNG']:
56
                $xpath = '//img[contains(@src,".png")]';
57
58
                break;
59
            case self::RESOURCE_TYPES['GIF']:
60
                $xpath = '//img[contains(@src,".gif")]';
61
62
                break;
63
            case self::RESOURCE_TYPES['ICO']:
64
                $xpath = '//link[contains(@href,".ico")]';
65
66
                break;
67
            case self::RESOURCE_TYPES['CSS']:
68
                $xpath = '//link[contains(@href,".css")]';
69
70
                break;
71
            case self::RESOURCE_TYPES['JAVASCRIPT']:
72
                $xpath = '//script[contains(@src,".js")]';
73
74
                break;
75
            case self::RESOURCE_TYPES['CSS_INLINE_HEAD']:
76
                $xpath = '//head//style';
77
78
                break;
79
            case self::RESOURCE_TYPES['CSS_LINK_HEAD']:
80
                $xpath = '//head//link[contains(@href,".css")]';
81
82
                break;
83
            default:
84
                throw new \Exception(
85
                    sprintf('TODO: Must implement %s resource type xpath constructor', $resourceType)
86
                );
87
        }
88
89
        if (true === $selfHosted) {
90
            $xpath = preg_replace(
91
                '/\[contains\(@(.*),/',
92
                '[(starts-with(@$1,"' . $this->webUrl . '") or starts-with(@$1,"/")) and contains(@$1,',
93
                $xpath
94
            );
95
        }
96
97
        $elements = $this->getSession()->getPage()->findAll('xpath', $xpath);
98
99
        if (true === $expected) {
100
            Assert::assertNotEmpty(
101
                $elements,
102
                sprintf(
103
                    'No%s %s files are found in %s',
104
                    $selfHosted ? ' self hosted' : '',
105
                    $resourceType,
106
                    $this->getCurrentUrl()
107
                )
108
            );
109
        }
110
111
        return $elements;
112
    }
113
114
    /**
115
     * @throws \Exception
116
     */
117
    private function getResourceUrl(NodeElement $element, string $resourceType): string
118
    {
119
        $this->assertResourceTypeIsValid($resourceType);
120
121
        switch ($resourceType) {
122
            case self::RESOURCE_TYPES['PNG']:
123
            case self::RESOURCE_TYPES['JPEG']:
124
            case self::RESOURCE_TYPES['GIF']:
125
            case self::RESOURCE_TYPES['JAVASCRIPT']:
126
                return $element->getAttribute('src');
127
128
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
129
            case self::RESOURCE_TYPES['CSS']:
130
            case self::RESOURCE_TYPES['ICO']:
131
                return $element->getAttribute('href');
132
133
                break;
134
            default:
135
                throw new \Exception(
136
                    sprintf('%s resource type url is not implemented', $resourceType)
137
                );
138
        }
139
    }
140
141
    private function assertResourceTypeIsValid(string $resourceType)
142
    {
143
        if (!in_array($resourceType, self::RESOURCE_TYPES)) {
144
            throw new \InvalidArgumentException(
145
                sprintf(
146
                    '%s resource type is not valid. Allowed types are: %s',
147
                    $resourceType,
148
                    implode(',', self::RESOURCE_TYPES)
149
                )
150
            );
151
        }
152
    }
153
154
    /**
155
     * @throws \Exception
156
     *
157
     * @Then HTML code should be minified
158
     */
159
    public function htmlShouldBeMinified()
160
    {
161
        $this->assertContentIsMinified(
162
            $this->getSession()->getPage()->getContent(),
163
            self::RESOURCE_TYPES['HTML']
164
        );
165
    }
166
167
    /**
168
     * @throws \Exception
169
     */
170
    private function assertContentIsMinified(string $content, string $resourceType)
171
    {
172
        switch ($resourceType) {
173
            case self::RESOURCE_TYPES['CSS']:
174
                $contentMinified = $this->minimizeCss($content);
175
176
                break;
177
            case self::RESOURCE_TYPES['JAVASCRIPT']:
178
                $contentMinified = $this->minimizeJs($content);
179
180
                break;
181
            case self::RESOURCE_TYPES['HTML']:
182
                $contentMinified = $this->minimizeHtml($content);
183
184
                break;
185
            default:
186
                throw new \Exception(
187
                    sprintf('Resource type "%s" can not be minified', $resourceType)
188
                );
189
        }
190
191
        Assert::assertTrue(
192
            $content == $contentMinified,
193
            sprintf(
194
                'Page %s %s code is not minified.',
195
                $this->getCurrentUrl(),
196
                $resourceType
197
            )
198
        );
199
    }
200
201
    private function minimizeCss(string $css): string
202
    {
203
        return preg_replace(
204
            [
205
                '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')|\/\*(?!\!)(?>.*?\*\/)|^\s*|\s*$#s',
206
                '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|\/\*(?>.*?\*\/))|\s*+;\s*+(})\s*+|\s*+([*$~^|
207
                ]?+=|[{};,>~+]|\s*+-(?![0-9\.])|!important\b)\s*+|([[(:])\s++|\s++([])])|\s++(:)\s*+(?!(?>[^{}"\']++|
208
                "(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')*+{)|^\s++|\s++\z|(\s)\s+#si',
209
            ],
210
            ['$1', '$1$2$3$4$5$6$7'],
211
            $css
212
        );
213
    }
214
215
    private function minimizeJs(string $js): string
216
    {
217
        return preg_replace(
218
            [
219
                '#\s*("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')\s*|\s*\/\*(?!\!|@cc_on)(?>[\s\S]*?\*\/)\s*|
220
                \s*(?<![\:\=])\/\/.*(?=[\n\r]|$)|^\s*|\s*$#',
221
                '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|\/\*(?>.*?\*\/)|\/(?!\/)[^\n\r]*?\/(?=[\s.,;]|
222
                [gimuy]|$))|\s*([!%&*\(\)\-=+\[\]\{\}|;:,.<>?\/])\s*#s',
223
            ],
224
            ['$1', '$1$2'],
225
            $js
226
        );
227
    }
228
229
    private function minimizeHtml(string $html): string
230
    {
231
        return preg_replace(
232
            '/(?<=>)\s+|\s+(?=<)/',
233
            '',
234
            $html
235
        );
236
    }
237
238
    /**
239
     * @throws \Exception
240
     *
241
     * @Then CSS code should load deferred
242
     */
243
    public function cssFilesShouldLoadDeferred()
244
    {
245
        $cssElements = $this->getPageResources(
246
            self::RESOURCE_TYPES['CSS_LINK_HEAD'],
247
            true,
248
            false
249
        );
250
251
        Assert::assertEmpty(
252
            $cssElements,
253
            sprintf(
254
                '%s self hosted css files are loading in head in %s',
255
                count($cssElements),
256
                $this->getCurrentUrl()
257
            )
258
        );
259
    }
260
261
    /**
262
     * @throws \Exception
263
     *
264
     * @Then critical CSS code should exist in head
265
     */
266
    public function criticalCssShouldExistInHead()
267
    {
268
        $styleCssElements = $this->getPageResources(
269
            self::RESOURCE_TYPES['CSS_INLINE_HEAD']
270
        );
271
272
        Assert::assertNotEmpty(
273
            $styleCssElements,
274
            sprintf(
275
                'No inline css is loading in head in %s',
276
                $this->getCurrentUrl()
277
            )
278
        );
279
    }
280
281
    /**
282
     * @Then HTML code should not be minified
283
     */
284
    public function htmlShouldNotBeMinified()
285
    {
286
        $this->assertInverse(
287
            [$this, 'htmlShouldBeMinified'],
288
            'HTML should not be minified.'
289
        );
290
    }
291
292
    /**
293
     * @Then /^(CSS|Javascript) code should not be minified$/
294
     */
295
    public function cssOrJavascriptFilesShouldNotBeMinified(string $resourceType)
296
    {
297
        $this->assertInverse(
298
            function () use ($resourceType) {
299
                $this->cssOrJavascriptFilesShouldBeMinified($resourceType);
300
            },
301
            sprintf('%s should not be minified.', $resourceType)
302
        );
303
    }
304
305
    /**
306
     * @throws \Exception
307
     * @throws UnsupportedDriverActionException
308
     *
309
     * @Then /^(CSS|Javascript) code should be minified$/
310
     */
311
    public function cssOrJavascriptFilesShouldBeMinified(string $resourceType)
312
    {
313
        $this->supportsSymfony(false);
314
315
        $resourceType = 'Javascript' === $resourceType ? 'js' : 'css';
316
317
        $elements = $this->getPageResources($resourceType);
318
        foreach ($elements as $element) {
319
            $elementUrl = $this->getResourceUrl($element, $resourceType);
320
321
            $this->getSession()->visit($elementUrl);
322
323
            $content = $this->getSession()->getPage()->getContent();
324
            $this->assertContentIsMinified($content, $resourceType);
325
326
            $this->getSession()->back();
327
        }
328
    }
329
330
    /**
331
     * @Then critical CSS code should not exist in head
332
     */
333
    public function criticalCssShouldNotExistInHead()
334
    {
335
        $this->assertInverse(
336
            [$this, 'criticalCssShouldExistInHead'],
337
            'Critical CSS exist in head.'
338
        );
339
    }
340
341
    /**
342
     * @Then /^browser cache should not be enabled for (png|jpeg|gif|ico|js|css) resources$/
343
     */
344
    public function browserCacheMustNotBeEnabledForCssResources(string $resourceType)
345
    {
346
        $this->assertInverse(
347
            function () use ($resourceType) {
348
                $this->browserCacheMustBeEnabledForResources($resourceType);
349
            },
350
            sprintf('Browser cache is enabled for %s resources.', $resourceType)
351
        );
352
    }
353
354
    /**
355
     * @throws \Exception
356
     *
357
     * @Then /^browser cache should be enabled for (png|jpeg|gif|ico|js|css) resources$/
358
     */
359
    public function browserCacheMustBeEnabledForResources(string $resourceType)
360
    {
361
        $this->supportsSymfony(false);
362
363
        $element = $this->getPageResources($resourceType);
364
        $element = count($element) ? current($element) : null;
365
366
        $elementUrl = $this->getResourceUrl($element, $resourceType);
0 ignored issues
show
Bug introduced by
It seems like $element can also be of type null; however, parameter $element of MOrtola\BehatSEOContexts...ntext::getResourceUrl() does only seem to accept Behat\Mink\Element\NodeElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

366
        $elementUrl = $this->getResourceUrl(/** @scrutinizer ignore-type */ $element, $resourceType);
Loading history...
367
368
        $this->getSession()->visit($elementUrl);
369
370
        $responseHeaders = $this->getSession()->getResponseHeaders();
371
372
        Assert::assertTrue(
373
            isset($responseHeaders['Cache-Control']),
374
            sprintf(
375
                'Browser cache is not enabled for %s resources. Cache-Control HTTP header was not received.',
376
                $resourceType
377
            )
378
        );
379
380
        Assert::assertNotContains(
381
            '-no',
382
            $responseHeaders['Cache-Control'],
383
            sprintf(
384
                'Browser cache is not enabled for %s resources. Cache-Control HTTP header is "no-cache".',
385
                $resourceType
386
            )
387
        );
388
389
        $this->getSession()->back();
390
    }
391
392
    /**
393
     * @Then /^Javascript code should not load (async|defer)$/
394
     */
395
    public function jsShouldNotLoadAsyncOr()
396
    {
397
        $this->assertInverse(
398
            [$this, 'javascriptFilesShouldLoadAsync'],
399
            'All JS files load async.'
400
        );
401
    }
402
}
403