Passed
Pull Request — master (#4)
by
unknown
02:03
created

browserCacheMustNotBeEnabledForResources()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 7
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 Behat\Symfony2Extension\Driver\KernelDriver;
8
use PHPUnit\Framework\Assert;
9
10
class PerformanceContext extends BaseContext
11
{
12
    const RES_EXT = [
13
        'PNG' => 'png',
14
        'HTML' => 'html',
15
        'JPEG' => 'jpeg',
16
        'GIF' => 'gif',
17
        'ICO' => 'ico',
18
        'JAVASCRIPT' => 'js',
19
        'CSS' => 'css',
20
        'CSS_INLINE_HEAD' => 'css-inline-head',
21
        'CSS_LINK_HEAD' => 'css-link-head',
22
    ];
23
24
    /**
25
     * @throws \Exception
26
     *
27
     * @Then /^Javascript code should load (async|defer)$/
28
     */
29
    public function javascriptFilesShouldLoadAsync()
30
    {
31
        foreach ($this->getPageResources(self::RES_EXT['JAVASCRIPT']) as $scriptElement) {
32
            Assert::assertTrue(
33
                $scriptElement->hasAttribute('async') || $scriptElement->hasAttribute('defer'),
34
                sprintf(
35
                    'Javascript file %s is render blocking in %s',
36
                    $this->getResourceUrl($scriptElement, self::RES_EXT['JAVASCRIPT']),
37
                    $this->getCurrentUrl()
38
                )
39
            );
40
        }
41
    }
42
43
    /**
44
     * @param string      $resourceType
45
     * @param bool        $selfHosted
46
     *
47
     * @param string|null $host
48
     *
49
     * @return NodeElement[]
50
     */
51
    private function getPageResources(string $resourceType, bool $selfHosted = true, string $host = null): array
52
    {
53
        if (!$xpath = $this->getResourceXpath($resourceType)) {
54
            return [];
55
        }
56
57
        if ($selfHosted) {
58
            $xpath = preg_replace(
59
                '/\[contains\(@(.*),/',
60
                sprintf('[(starts-with(@$1,"%s") or starts-with(@$1,"/")) and contains(@$1,', $this->webUrl),
61
                $xpath
62
            );
63
        } elseif ($host === 'external') {
64
            $xpath = preg_replace(
65
                '/\[contains\(@(.*),/',
66
                '[not(starts-with(@$1,"' . $this->webUrl . '") or starts-with(@$1,"/")) and contains(@$1,',
67
                $xpath
68
            );
69
        } elseif (null !== $host) {
70
            $xpath = preg_replace(
71
                '/\[contains\(@(.*),/',
72
                '[(starts-with(@$1,"' . $host . '") or starts-with(@$1,"/")) and contains(@$1,',
73
                $xpath
74
            );
75
        }
76
77
        return $this->getSession()->getPage()->findAll('xpath', $xpath);
78
    }
79
80
    private function getResourceXpath(string $resourceType): string
81
    {
82
        if (in_array($resourceType, [self::RES_EXT['JPEG'], self::RES_EXT['PNG'], self::RES_EXT['GIF']], true)) {
83
            return sprintf('//img[contains(@src,".%s")]', $resourceType);
84
        }
85
        if (in_array($resourceType, [self::RES_EXT['ICO'], self::RES_EXT['CSS']], true)) {
86
            return sprintf('//link[contains(@href,".%s")]', $resourceType);
87
        }
88
        if (self::RES_EXT['JAVASCRIPT'] === $resourceType) {
89
            return '//script[contains(@src,".js")]';
90
        }
91
        if (self::RES_EXT['CSS_INLINE_HEAD'] === $resourceType) {
92
            return '//head//style';
93
        }
94
        if (self::RES_EXT['CSS_LINK_HEAD'] === $resourceType) {
95
            return '//head//link[contains(@href,".css")]';
96
        }
97
98
        return '';
99
    }
100
101
    /**
102
     * @param NodeElement $element
103
     * @param string      $resourceType
104
     *
105
     * @return string
106
     * @throws \Exception
107
     */
108
    private function getResourceUrl(NodeElement $element, string $resourceType): string
109
    {
110
        $this->assertResourceTypeIsValid($resourceType);
111
112
        if (in_array($resourceType, [
113
            self::RES_EXT['PNG'],
114
            self::RES_EXT['JPEG'],
115
            self::RES_EXT['GIF'],
116
            self::RES_EXT['JAVASCRIPT']
117
        ], true)) {
118
            return $element->getAttribute('src');
119
        }
120
121
        if (in_array($resourceType, [self::RES_EXT['CSS'], self::RES_EXT['ICO']], true)) {
122
            return $element->getAttribute('href');
123
        }
124
125
        throw new \Exception(
126
            sprintf('%s resource type url is not implemented', $resourceType)
127
        );
128
    }
129
130
    private function assertResourceTypeIsValid(string $resourceType)
131
    {
132
        if (!in_array($resourceType, self::RES_EXT, true)) {
133
            throw new \InvalidArgumentException(
134
                sprintf(
135
                    '%s resource type is not valid. Allowed types are: %s',
136
                    $resourceType,
137
                    implode(',', self::RES_EXT)
138
                )
139
            );
140
        }
141
    }
142
143
    /**
144
     * @throws \Exception
145
     *
146
     * @Then HTML code should be minified
147
     */
148
    public function htmlShouldBeMinified()
149
    {
150
        $this->assertContentIsMinified(
151
            $this->getSession()->getPage()->getContent(),
152
            $this->minimizeHtml($this->getSession()->getPage()->getContent())
153
        );
154
    }
155
156
    private function assertContentIsMinified(string $content, string $contentMinified)
157
    {
158
        Assert::assertSame(
159
            $content,
160
            $contentMinified,
161
            'Code is not minified.'
162
        );
163
    }
164
165
    private function minimizeCss(string $css): string
166
    {
167
        return preg_replace(
168
            [
169
                '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')|\/\*(?!\!)(?>.*?\*\/)|^\s*|\s*$#s',
170
                '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|\/\*(?>.*?\*\/))|\s*+;\s*+(})\s*+|\s*+([*$~^|
171
                ]?+=|[{};,>~+]|\s*+-(?![0-9\.])|!important\b)\s*+|([[(:])\s++|\s++([])])|\s++(:)\s*+(?!(?>[^{}"\']++|
172
                "(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')*+{)|^\s++|\s++\z|(\s)\s+#si',
173
            ],
174
            ['$1', '$1$2$3$4$5$6$7'],
175
            $css
176
        );
177
    }
178
179
    private function minimizeJs(string $js): string
180
    {
181
        return preg_replace(
182
            [
183
                '#\s*("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')\s*|\s*\/\*(?!\!|@cc_on)(?>[\s\S]*?\*\/)\s*|
184
                \s*(?<![\:\=])\/\/.*(?=[\n\r]|$)|^\s*|\s*$#',
185
                '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|\/\*(?>.*?\*\/)|\/(?!\/)[^\n\r]*?\/(?=[\s.,;]|
186
                [gimuy]|$))|\s*([!%&*\(\)\-=+\[\]\{\}|;:,.<>?\/])\s*#s',
187
            ],
188
            ['$1', '$1$2'],
189
            $js
190
        );
191
    }
192
193
    private function minimizeHtml(string $html): string
194
    {
195
        return preg_replace(
196
            '/(?<=>)\s+|\s+(?=<)/',
197
            '',
198
            $html
199
        );
200
    }
201
202
    /**
203
     * @throws \Exception
204
     *
205
     * @Then CSS code should load deferred
206
     */
207
    public function cssFilesShouldLoadDeferred()
208
    {
209
        Assert::assertEmpty(
210
            $this->getPageResources(self::RES_EXT['CSS_LINK_HEAD'], true),
211
            sprintf(
212
                'Some self hosted css files are loading in head in %s',
213
                $this->getCurrentUrl()
214
            )
215
        );
216
    }
217
218
    /**
219
     * @throws \Exception
220
     *
221
     * @Then critical CSS code should exist in head
222
     */
223
    public function criticalCssShouldExistInHead()
224
    {
225
        Assert::assertNotEmpty(
226
            $this->getPageResources(self::RES_EXT['CSS_INLINE_HEAD']),
227
            sprintf(
228
                'No inline css is loading in head in %s',
229
                $this->getCurrentUrl()
230
            )
231
        );
232
    }
233
234
    /**
235
     * @Then HTML code should not be minified
236
     */
237
    public function htmlShouldNotBeMinified()
238
    {
239
        $this->assertInverse(
240
            [$this, 'htmlShouldBeMinified'],
241
            'HTML should not be minified.'
242
        );
243
    }
244
245
    /**
246
     * @Then /^(CSS|Javascript) code should not be minified$/
247
     * @param string $resourceType
248
     */
249
    public function cssOrJavascriptFilesShouldNotBeMinified(string $resourceType)
250
    {
251
        $this->assertInverse(
252
            function () use ($resourceType) {
253
                $this->cssOrJavascriptFilesShouldBeMinified($resourceType);
254
            },
255
            sprintf('%s should not be minified.', $resourceType)
256
        );
257
    }
258
259
    /**
260
     * @param string $resourceType
261
     *
262
     * @throws UnsupportedDriverActionException
263
     * @throws \Exception
264
     * @Then /^(CSS|Javascript) code should be minified$/
265
     */
266
    public function cssOrJavascriptFilesShouldBeMinified(string $resourceType)
267
    {
268
        $this->doesNotSupportDriver(KernelDriver::class);
269
270
        $resourceType = 'Javascript' === $resourceType ? 'js' : 'css';
271
272
        foreach ($this->getPageResources($resourceType) as $element) {
273
            $this->getSession()->visit($this->getResourceUrl($element, $resourceType));
274
275
            $this->assertContentIsMinified(
276
                $this->getSession()->getPage()->getContent(),
277
                'js' === $resourceType ?
278
                    $this->minimizeJs($this->getSession()->getPage()->getContent())
279
                    : $this->minimizeCss($this->getSession()->getPage()->getContent())
280
            );
281
282
            $this->getSession()->back();
283
        }
284
    }
285
286
    /**
287
     * @Then critical CSS code should not exist in head
288
     */
289
    public function criticalCssShouldNotExistInHead()
290
    {
291
        $this->assertInverse(
292
            [$this, 'criticalCssShouldExistInHead'],
293
            'Critical CSS exist in head.'
294
        );
295
    }
296
297
    /**
298
     * @Then /^browser cache should not be enabled for (.+|external|internal) (png|jpeg|gif|ico|js|css) resources$/
299
     * @param string $host
300
     * @param string $resourceType
301
     */
302
    public function browserCacheMustNotBeEnabledForResources(string $host, string $resourceType)
303
    {
304
        $this->assertInverse(
305
            function () use ($host, $resourceType) {
306
                $this->browserCacheMustBeEnabledForResources($host, $resourceType);
307
            },
308
            sprintf('Browser cache is enabled for %s resources.', $resourceType)
309
        );
310
    }
311
312
    /**
313
     * @param string $host
314
     * @param string $resourceType
315
     *
316
     * @throws UnsupportedDriverActionException
317
     * @throws \Exception
318
     * @Then /^browser cache should be enabled for (.+|external|internal) (png|jpeg|gif|ico|js|css) resources$/
319
     */
320
    public function browserCacheMustBeEnabledForResources(string $host, string $resourceType)
321
    {
322
        $this->doesNotSupportDriver(KernelDriver::class);
323
        switch ($host) {
324
        case 'internal':
325
            $elements = $this->getPageResources($resourceType, true);
326
                break;
327
        case 'external':
328
            $elements = $this->getPageResources($resourceType, false, $host);
329
                break;
330
        default:
331
            $elements = $this->getPageResources($resourceType, false, $host);
332
                break;
333
        }
334
        $this->checkResourceCache($elements[array_rand($elements)], $resourceType);
335
        $this->getSession()->back();
336
    }
337
338
    /**
339
     * @Then /^Javascript code should not load (async|defer)$/
340
     */
341
    public function jsShouldNotLoadAsyncOr()
342
    {
343
        $this->assertInverse(
344
            [$this, 'javascriptFilesShouldLoadAsync'],
345
            'All JS files load async.'
346
        );
347
    }
348
349
    /**
350
     * @param        $element
351
     * @param string $resourceType
352
     *
353
     * @throws \Exception
354
     */
355
    private function checkResourceCache($element, $resourceType)
356
    {
357
        $this->getSession()->visit($this->getResourceUrl($element, $resourceType));
358
        $headers = array_change_key_case($this->getSession()->getResponseHeaders());
359
360
        Assert::assertTrue(
361
            array_key_exists('cache-control', $headers),
362
            sprintf(
363
                'Browser cache is not enabled for %s resources. Cache-Control HTTP header was not received.',
364
                $resourceType
365
            )
366
        );
367
368
        Assert::assertNotContains(
369
            '-no',
370
            $headers['cache-control'],
371
            sprintf(
372
                'Browser cache is not enabled for %s resources. Cache-Control HTTP header is "no-cache".',
373
                $resourceType
374
            )
375
        );
376
    }
377
}
378