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

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

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