Passed
Push — feature/tests ( 9b7685...30969f )
by Marc
02:06
created

PerformanceContext::getResourceUrl()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 3
nop 2
dl 0
loc 19
rs 9.9
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
     * @return NodeElement[]
45
     */
46
    private function getPageResources(string $resourceType, bool $selfHosted = true): array
47
    {
48
        if (in_array($resourceType, [self::RES_EXT['JPEG'], self::RES_EXT['PNG'], self::RES_EXT['GIF']])) {
49
            $xpath = sprintf('//img[contains(@src,".%s")]', $resourceType);
50
        } elseif (in_array($resourceType, [self::RES_EXT['ICO'], self::RES_EXT['CSS']])) {
51
            $xpath = sprintf('//link[contains(@href,".%s")]', $resourceType);
52
        } elseif (self::RES_EXT['JAVASCRIPT'] === $resourceType) {
53
            $xpath = '//script[contains(@src,".js")]';
54
        } elseif (self::RES_EXT['CSS_INLINE_HEAD'] === $resourceType) {
55
            $xpath = '//head//style';
56
        } elseif (self::RES_EXT['CSS_LINK_HEAD'] === $resourceType) {
57
            $xpath = '//head//link[contains(@href,".css")]';
58
        }
59
60
        if (!isset($xpath)) {
61
            return [];
62
        }
63
64
        if ($selfHosted) {
65
            $xpath = preg_replace(
66
                '/\[contains\(@(.*),/',
67
                sprintf('[(starts-with(@$1,"%s") or starts-with(@$1,"/")) and contains(@$1,', $this->webUrl),
68
                $xpath
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $xpath does not seem to be defined for all execution paths leading up to this point.
Loading history...
69
            );
70
        }
71
72
        return $this->getSession()->getPage()->findAll('xpath', $xpath);
73
    }
74
75
    /**
76
     * @throws \Exception
77
     */
78
    private function getResourceUrl(NodeElement $element, string $resourceType): string
79
    {
80
        $this->assertResourceTypeIsValid($resourceType);
81
82
        if (in_array($resourceType, [
83
            self::RES_EXT['PNG'],
84
            self::RES_EXT['JPEG'],
85
            self::RES_EXT['GIF'],
86
            self::RES_EXT['JAVASCRIPT']
87
        ])) {
88
            return $element->getAttribute('src');
89
        }
90
91
        if (in_array($resourceType, [self::RES_EXT['CSS'], self::RES_EXT['ICO']])) {
92
            return $element->getAttribute('href');
93
        }
94
95
        throw new \Exception(
96
            sprintf('%s resource type url is not implemented', $resourceType)
97
        );
98
    }
99
100
    private function assertResourceTypeIsValid(string $resourceType)
101
    {
102
        if (!in_array($resourceType, self::RES_EXT)) {
103
            throw new \InvalidArgumentException(
104
                sprintf(
105
                    '%s resource type is not valid. Allowed types are: %s',
106
                    $resourceType,
107
                    implode(',', self::RES_EXT)
108
                )
109
            );
110
        }
111
    }
112
113
    /**
114
     * @throws \Exception
115
     *
116
     * @Then HTML code should be minified
117
     */
118
    public function htmlShouldBeMinified()
119
    {
120
        $this->assertContentIsMinified(
121
            $this->getSession()->getPage()->getContent(),
122
            $this->minimizeHtml($this->getSession()->getPage()->getContent())
123
        );
124
    }
125
126
    private function assertContentIsMinified(string $content, string $contentMinified)
127
    {
128
        Assert::assertTrue(
129
            $content == $contentMinified,
130
            'Code is not minified.'
131
        );
132
    }
133
134
    private function minimizeCss(string $css): string
135
    {
136
        return preg_replace(
137
            [
138
                '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')|\/\*(?!\!)(?>.*?\*\/)|^\s*|\s*$#s',
139
                '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|\/\*(?>.*?\*\/))|\s*+;\s*+(})\s*+|\s*+([*$~^|
140
                ]?+=|[{};,>~+]|\s*+-(?![0-9\.])|!important\b)\s*+|([[(:])\s++|\s++([])])|\s++(:)\s*+(?!(?>[^{}"\']++|
141
                "(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')*+{)|^\s++|\s++\z|(\s)\s+#si',
142
            ],
143
            ['$1', '$1$2$3$4$5$6$7'],
144
            $css
145
        );
146
    }
147
148
    private function minimizeJs(string $js): string
149
    {
150
        return preg_replace(
151
            [
152
                '#\s*("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')\s*|\s*\/\*(?!\!|@cc_on)(?>[\s\S]*?\*\/)\s*|
153
                \s*(?<![\:\=])\/\/.*(?=[\n\r]|$)|^\s*|\s*$#',
154
                '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|\/\*(?>.*?\*\/)|\/(?!\/)[^\n\r]*?\/(?=[\s.,;]|
155
                [gimuy]|$))|\s*([!%&*\(\)\-=+\[\]\{\}|;:,.<>?\/])\s*#s',
156
            ],
157
            ['$1', '$1$2'],
158
            $js
159
        );
160
    }
161
162
    private function minimizeHtml(string $html): string
163
    {
164
        return preg_replace(
165
            '/(?<=>)\s+|\s+(?=<)/',
166
            '',
167
            $html
168
        );
169
    }
170
171
    /**
172
     * @throws \Exception
173
     *
174
     * @Then CSS code should load deferred
175
     */
176
    public function cssFilesShouldLoadDeferred()
177
    {
178
        Assert::assertEmpty(
179
            $this->getPageResources(self::RES_EXT['CSS_LINK_HEAD'], true),
180
            sprintf(
181
                'Some self hosted css files are loading in head in %s',
182
                $this->getCurrentUrl()
183
            )
184
        );
185
    }
186
187
    /**
188
     * @throws \Exception
189
     *
190
     * @Then critical CSS code should exist in head
191
     */
192
    public function criticalCssShouldExistInHead()
193
    {
194
        Assert::assertNotEmpty(
195
            $this->getPageResources(self::RES_EXT['CSS_INLINE_HEAD']),
196
            sprintf(
197
                'No inline css is loading in head in %s',
198
                $this->getCurrentUrl()
199
            )
200
        );
201
    }
202
203
    /**
204
     * @Then HTML code should not be minified
205
     */
206
    public function htmlShouldNotBeMinified()
207
    {
208
        $this->assertInverse(
209
            [$this, 'htmlShouldBeMinified'],
210
            'HTML should not be minified.'
211
        );
212
    }
213
214
    /**
215
     * @Then /^(CSS|Javascript) code should not be minified$/
216
     */
217
    public function cssOrJavascriptFilesShouldNotBeMinified(string $resourceType)
218
    {
219
        $this->assertInverse(
220
            function () use ($resourceType) {
221
                $this->cssOrJavascriptFilesShouldBeMinified($resourceType);
222
            },
223
            sprintf('%s should not be minified.', $resourceType)
224
        );
225
    }
226
227
    /**
228
     * @throws \Exception
229
     * @throws UnsupportedDriverActionException
230
     *
231
     * @Then /^(CSS|Javascript) code should be minified$/
232
     */
233
    public function cssOrJavascriptFilesShouldBeMinified(string $resourceType)
234
    {
235
        $this->doesNotSupportDriver(KernelDriver::class);
236
237
        $resourceType = 'Javascript' === $resourceType ? 'js' : 'css';
238
239
        foreach ($this->getPageResources($resourceType) as $element) {
240
            $this->getSession()->visit($this->getResourceUrl($element, $resourceType));
241
242
            $this->assertContentIsMinified(
243
                $this->getSession()->getPage()->getContent(),
244
                'js' === $resourceType ?
245
                    $this->minimizeJs($this->getSession()->getPage()->getContent())
246
                    : $this->minimizeCss($this->getSession()->getPage()->getContent())
247
            );
248
249
            $this->getSession()->back();
250
        }
251
    }
252
253
    /**
254
     * @Then critical CSS code should not exist in head
255
     */
256
    public function criticalCssShouldNotExistInHead()
257
    {
258
        $this->assertInverse(
259
            [$this, 'criticalCssShouldExistInHead'],
260
            'Critical CSS exist in head.'
261
        );
262
    }
263
264
    /**
265
     * @Then /^browser cache should not be enabled for (png|jpeg|gif|ico|js|css) resources$/
266
     */
267
    public function browserCacheMustNotBeEnabledForCssResources(string $resourceType)
268
    {
269
        $this->assertInverse(
270
            function () use ($resourceType) {
271
                $this->browserCacheMustBeEnabledForResources($resourceType);
272
            },
273
            sprintf('Browser cache is enabled for %s resources.', $resourceType)
274
        );
275
    }
276
277
    /**
278
     * @throws \Exception
279
     *
280
     * @Then /^browser cache should be enabled for (png|jpeg|gif|ico|js|css) resources$/
281
     */
282
    public function browserCacheMustBeEnabledForResources(string $resourceType)
283
    {
284
        $this->doesNotSupportDriver(KernelDriver::class);
285
286
        $element = $this->getPageResources($resourceType);
287
        $element = 0 < count($element) ? current($element) : null;
288
289
        $this->getSession()->visit($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

289
        $this->getSession()->visit($this->getResourceUrl(/** @scrutinizer ignore-type */ $element, $resourceType));
Loading history...
290
291
        Assert::assertTrue(
292
            isset($this->getSession()->getResponseHeaders()['Cache-Control']),
293
            sprintf(
294
                'Browser cache is not enabled for %s resources. Cache-Control HTTP header was not received.',
295
                $resourceType
296
            )
297
        );
298
299
        Assert::assertNotContains(
300
            '-no',
301
            $this->getSession()->getResponseHeaders()['Cache-Control'],
302
            sprintf(
303
                'Browser cache is not enabled for %s resources. Cache-Control HTTP header is "no-cache".',
304
                $resourceType
305
            )
306
        );
307
308
        $this->getSession()->back();
309
    }
310
311
    /**
312
     * @Then /^Javascript code should not load (async|defer)$/
313
     */
314
    public function jsShouldNotLoadAsyncOr()
315
    {
316
        $this->assertInverse(
317
            [$this, 'javascriptFilesShouldLoadAsync'],
318
            'All JS files load async.'
319
        );
320
    }
321
}
322