Passed
Pull Request — 4 (#10222)
by Steve
07:01
created

FunctionalTest::assertPartialHTMLMatchBySelector()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 13
nc 8
nop 3
dl 0
loc 23
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Dev;
4
5
use PHPUnit_Framework_AssertionFailedError;
0 ignored issues
show
Bug introduced by
The type PHPUnit_Framework_AssertionFailedError was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use PHPUnit_Extensions_GroupTestSuite;
0 ignored issues
show
Bug introduced by
The type PHPUnit_Extensions_GroupTestSuite was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use PHPUnit\Framework\AssertionFailedError;
8
use PHPUnit\Framework\Constraint\IsEqualCanonicalizing;
9
use SilverStripe\Control\Director;
10
use SilverStripe\Control\HTTPResponse;
11
use SilverStripe\Control\Session;
12
use SilverStripe\Core\Config\Config;
13
use SilverStripe\Security\BasicAuth;
14
use SilverStripe\Security\SecurityToken;
15
use SilverStripe\View\SSViewer;
16
use SimpleXMLElement;
17
18
/* -------------------------------------------------
19
 *
20
 * This version of FunctionalTest is for phpunit 9
21
 * The phpunit 5 version is lower down in this file
22
 * phpunit 6, 7 and 8 are not supported
23
 *
24
 * @see SilverStripe\Dev\SapphireTest
25
 *
26
 * IsEqualCanonicalizing::class is a new class added in PHPUnit 9, testing that this class exists
27
 * to ensure that we're not using a a prior, incompatible version of PHPUnit
28
 *
29
 * -------------------------------------------------
30
 */
31
if (class_exists(IsEqualCanonicalizing::class)) {
32
33
    /**
34
     * SilverStripe-specific testing object designed to support functional testing of your web app.  It simulates get/post
35
     * requests, form submission, and can validate resulting HTML, looking up content by CSS selector.
36
     *
37
     * The example below shows how it works.
38
     *
39
     * <code>
40
     *   public function testMyForm() {
41
     *   // Visit a URL
42
     *   $this->get("your/url");
43
     *
44
     *   // Submit a form on the page that you get in response
45
     *   $this->submitForm("MyForm_ID", "action_dologin", array("Email" => "invalid email ^&*&^"));
46
     *
47
     *   // Validate the content that is returned
48
     *   $this->assertExactMatchBySelector("#MyForm_ID p.error", array("That email address is invalid."));
49
     *  }
50
     * </code>
51
     */
52
    // Ignore multiple classes in same file
53
    // @codingStandardsIgnoreStart
54
    class FunctionalTest extends SapphireTest implements TestOnly
55
    {
56
        // @codingStandardsIgnoreEnd
57
        /**
58
         * Set this to true on your sub-class to disable the use of themes in this test.
59
         * This can be handy for functional testing of modules without having to worry about whether a user has changed
60
         * behaviour by replacing the theme.
61
         *
62
         * @var bool
63
         */
64
        protected static $disable_themes = false;
65
66
        /**
67
         * Set this to true on your sub-class to use the draft site by default for every test in this class.
68
         *
69
         * @deprecated 4.2.0:5.0.0 Use ?stage=Stage in your ->get() querystring requests instead
70
         * @var bool
71
         */
72
        protected static $use_draft_site = false;
73
74
        /**
75
         * @var TestSession
76
         */
77
        protected $mainSession = null;
78
79
        /**
80
         * CSSContentParser for the most recently requested page.
81
         *
82
         * @var CSSContentParser
83
         */
84
        protected $cssParser = null;
85
86
        /**
87
         * If this is true, then 30x Location headers will be automatically followed.
88
         * If not, then you will have to manually call $this->mainSession->followRedirection() to follow them.
89
         * However, this will let you inspect the intermediary headers
90
         *
91
         * @var bool
92
         */
93
        protected $autoFollowRedirection = true;
94
95
        /**
96
         * Returns the {@link Session} object for this test
97
         *
98
         * @return Session
99
         */
100
        public function session()
101
        {
102
            return $this->mainSession->session();
103
        }
104
105
        protected function setUp(): void
106
        {
107
            parent::setUp();
108
109
            // Skip calling FunctionalTest directly.
110
            if (static::class == __CLASS__) {
0 ignored issues
show
introduced by
The condition static::class == __CLASS__ is always true.
Loading history...
111
                $this->markTestSkipped(sprintf('Skipping %s ', static::class));
112
            }
113
114
            $this->mainSession = new TestSession();
115
116
            // Disable theme, if necessary
117
            if (static::get_disable_themes()) {
118
                SSViewer::config()->update('theme_enabled', false);
119
            }
120
121
            // Flush user
122
            $this->logOut();
123
124
            // Switch to draft site, if necessary
125
            // If you rely on this you should be crafting stage-specific urls instead though.
126
            if (static::get_use_draft_site()) {
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Dev\Functio...t::get_use_draft_site() has been deprecated: 4.2.0:5.0.0 Use ?stage=Stage in your querystring arguments instead ( Ignorable by Annotation )

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

126
            if (/** @scrutinizer ignore-deprecated */ static::get_use_draft_site()) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
127
                $this->useDraftSite();
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Dev\FunctionalTest::useDraftSite() has been deprecated: 4.2.0:5.0.0 Use ?stage=Stage querystring arguments instead of useDraftSite ( Ignorable by Annotation )

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

127
                /** @scrutinizer ignore-deprecated */ $this->useDraftSite();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
128
            }
129
130
            // Unprotect the site, tests are running with the assumption it's off. They will enable it on a case-by-case
131
            // basis.
132
            BasicAuth::protect_entire_site(false);
133
134
            SecurityToken::disable();
135
        }
136
137
        protected function tearDown(): void
138
        {
139
            SecurityToken::enable();
140
            unset($this->mainSession);
141
            parent::tearDown();
142
        }
143
144
        /**
145
         * Run a test while mocking the base url with the provided value
146
         * @param string $url The base URL to use for this test
147
         * @param callable $callback The test to run
148
         */
149
        protected function withBaseURL($url, $callback)
150
        {
151
            $oldBase = Config::inst()->get(Director::class, 'alternate_base_url');
152
            Config::modify()->set(Director::class, 'alternate_base_url', $url);
153
            $callback($this);
154
            Config::modify()->set(Director::class, 'alternate_base_url', $oldBase);
155
        }
156
157
        /**
158
         * Run a test while mocking the base folder with the provided value
159
         * @param string $folder The base folder to use for this test
160
         * @param callable $callback The test to run
161
         */
162
        protected function withBaseFolder($folder, $callback)
163
        {
164
            $oldFolder = Config::inst()->get(Director::class, 'alternate_base_folder');
165
            Config::modify()->set(Director::class, 'alternate_base_folder', $folder);
166
            $callback($this);
167
            Config::modify()->set(Director::class, 'alternate_base_folder', $oldFolder);
168
        }
169
170
        /**
171
         * Submit a get request
172
         * @uses Director::test()
173
         *
174
         * @param string $url
175
         * @param Session $session
176
         * @param array $headers
177
         * @param array $cookies
178
         * @return HTTPResponse
179
         */
180
        public function get($url, $session = null, $headers = null, $cookies = null)
181
        {
182
            $this->cssParser = null;
183
            $response = $this->mainSession->get($url, $session, $headers, $cookies);
184
            if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) {
185
                $response = $this->mainSession->followRedirection();
186
            }
187
            return $response;
188
        }
189
190
        /**
191
         * Submit a post request
192
         *
193
         * @uses Director::test()
194
         * @param string $url
195
         * @param array $data
196
         * @param array $headers
197
         * @param Session $session
198
         * @param string $body
199
         * @param array $cookies
200
         * @return HTTPResponse
201
         */
202
        public function post($url, $data, $headers = null, $session = null, $body = null, $cookies = null)
203
        {
204
            $this->cssParser = null;
205
            $response = $this->mainSession->post($url, $data, $headers, $session, $body, $cookies);
206
            if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) {
207
                $response = $this->mainSession->followRedirection();
208
            }
209
            return $response;
210
        }
211
212
        /**
213
         * Submit the form with the given HTML ID, filling it out with the given data.
214
         * Acts on the most recent response.
215
         *
216
         * Any data parameters have to be present in the form, with exact form field name
217
         * and values, otherwise they are removed from the submission.
218
         *
219
         * Caution: Parameter names have to be formatted
220
         * as they are in the form submission, not as they are interpreted by PHP.
221
         * Wrong: array('mycheckboxvalues' => array(1 => 'one', 2 => 'two'))
222
         * Right: array('mycheckboxvalues[1]' => 'one', 'mycheckboxvalues[2]' => 'two')
223
         *
224
         * @see http://www.simpletest.org/en/form_testing_documentation.html
225
         *
226
         * @param string $formID HTML 'id' attribute of a form (loaded through a previous response)
227
         * @param string $button HTML 'name' attribute of the button (NOT the 'id' attribute)
228
         * @param array $data Map of GET/POST data.
229
         * @return HTTPResponse
230
         */
231
        public function submitForm($formID, $button = null, $data = [])
232
        {
233
            $this->cssParser = null;
234
            $response = $this->mainSession->submitForm($formID, $button, $data);
235
            if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) {
236
                $response = $this->mainSession->followRedirection();
237
            }
238
            return $response;
239
        }
240
241
        /**
242
         * Return the most recent content
243
         *
244
         * @return string
245
         */
246
        public function content()
247
        {
248
            return $this->mainSession->lastContent();
249
        }
250
251
        /**
252
         * Find an attribute in a SimpleXMLElement object by name.
253
         * @param SimpleXMLElement $object
254
         * @param string $attribute Name of attribute to find
255
         * @return SimpleXMLElement object of the attribute
256
         */
257
        public function findAttribute($object, $attribute)
258
        {
259
            $found = false;
260
            foreach ($object->attributes() as $a => $b) {
261
                if ($a == $attribute) {
262
                    $found = $b;
263
                }
264
            }
265
            return $found;
266
        }
267
268
        /**
269
         * Return a CSSContentParser for the most recent content.
270
         *
271
         * @return CSSContentParser
272
         */
273
        public function cssParser()
274
        {
275
            if (!$this->cssParser) {
276
                $this->cssParser = new CSSContentParser($this->mainSession->lastContent());
277
            }
278
            return $this->cssParser;
279
        }
280
281
        /**
282
         * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
283
         * The given CSS selector will be applied to the HTML of the most recent page.  The content of every matching tag
284
         * will be examined. The assertion fails if one of the expectedMatches fails to appear.
285
         *
286
         * Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account.
287
         *
288
         * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
289
         * @param array|string $expectedMatches The content of at least one of the matched tags
290
         * @param string $message
291
         * @throws AssertionFailedError
292
         */
293
        public function assertPartialMatchBySelector($selector, $expectedMatches, $message = null)
294
        {
295
            if (is_string($expectedMatches)) {
296
                $expectedMatches = [$expectedMatches];
297
            }
298
299
            $items = $this->cssParser()->getBySelector($selector);
300
301
            $actuals = [];
302
            if ($items) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $items of type SimpleXMLElement[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
303
                foreach ($items as $item) {
304
                    $actuals[trim(preg_replace('/\s+/', ' ', (string)$item))] = true;
305
                }
306
            }
307
308
            $message = $message ?:
309
            "Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'"
310
                . implode("'\n'", $expectedMatches) . "'\n\n"
311
                . "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals)) . "'";
312
313
            foreach ($expectedMatches as $match) {
314
                $this->assertTrue(isset($actuals[$match]), $message);
315
            }
316
        }
317
318
        /**
319
         * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
320
         * The given CSS selector will be applied to the HTML of the most recent page.  The full HTML of every matching tag
321
         * will be examined. The assertion fails if one of the expectedMatches fails to appear.
322
         *
323
         * Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account.
324
         *
325
         * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
326
         * @param array|string $expectedMatches The content of *all* matching tags as an array
327
         * @param string $message
328
         * @throws AssertionFailedError
329
         */
330
        public function assertExactMatchBySelector($selector, $expectedMatches, $message = null)
331
        {
332
            if (is_string($expectedMatches)) {
333
                $expectedMatches = [$expectedMatches];
334
            }
335
336
            $items = $this->cssParser()->getBySelector($selector);
337
338
            $actuals = [];
339
            if ($items) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $items of type SimpleXMLElement[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
340
                foreach ($items as $item) {
341
                    $actuals[] = trim(preg_replace('/\s+/', ' ', (string)$item));
342
                }
343
            }
344
345
            $message = $message ?:
346
                    "Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'"
347
                    . implode("'\n'", $expectedMatches) . "'\n\n"
348
                . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'";
349
350
            $this->assertTrue($expectedMatches == $actuals, $message);
351
        }
352
353
        /**
354
         * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
355
         * The given CSS selector will be applied to the HTML of the most recent page.  The content of every matching tag
356
         * will be examined. The assertion fails if one of the expectedMatches fails to appear.
357
         *
358
         * Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account.
359
         *
360
         * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
361
         * @param array|string $expectedMatches The content of at least one of the matched tags
362
         * @param string $message
363
         * @throws AssertionFailedError
364
         */
365
        public function assertPartialHTMLMatchBySelector($selector, $expectedMatches, $message = null)
366
        {
367
            if (is_string($expectedMatches)) {
368
                $expectedMatches = [$expectedMatches];
369
            }
370
371
            $items = $this->cssParser()->getBySelector($selector);
372
373
            $actuals = [];
374
            if ($items) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $items of type SimpleXMLElement[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
375
                /** @var SimpleXMLElement $item */
376
                foreach ($items as $item) {
377
                    $actuals[$item->asXML()] = true;
378
                }
379
            }
380
381
            $message = $message ?:
382
                    "Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'"
383
                    . implode("'\n'", $expectedMatches) . "'\n\n"
384
                . "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals)) . "'";
385
386
            foreach ($expectedMatches as $match) {
387
                $this->assertTrue(isset($actuals[$match]), $message);
388
            }
389
        }
390
391
        /**
392
         * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
393
         * The given CSS selector will be applied to the HTML of the most recent page.  The full HTML of every matching tag
394
         * will be examined. The assertion fails if one of the expectedMatches fails to appear.
395
         *
396
         * Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account.
397
         *
398
         * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
399
         * @param array|string $expectedMatches The content of *all* matched tags as an array
400
         * @param string $message
401
         * @throws AssertionFailedError
402
         */
403
        public function assertExactHTMLMatchBySelector($selector, $expectedMatches, $message = null)
404
        {
405
            $items = $this->cssParser()->getBySelector($selector);
406
407
            $actuals = [];
408
            if ($items) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $items of type SimpleXMLElement[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
409
                /** @var SimpleXMLElement $item */
410
                foreach ($items as $item) {
411
                    $actuals[] = $item->asXML();
412
                }
413
            }
414
415
            $message = $message ?:
416
                "Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'"
417
                . implode("'\n'", $expectedMatches) . "'\n\n"
0 ignored issues
show
Bug introduced by
It seems like $expectedMatches can also be of type string; however, parameter $pieces of implode() does only seem to accept array, 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

417
                . implode("'\n'", /** @scrutinizer ignore-type */ $expectedMatches) . "'\n\n"
Loading history...
418
                . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'";
419
420
            $this->assertTrue($expectedMatches == $actuals, $message);
421
        }
422
423
        /**
424
         * Use the draft (stage) site for testing.
425
         * This is helpful if you're not testing publication functionality and don't want "stage management" cluttering
426
         * your test.
427
         *
428
         * @deprecated 4.2.0:5.0.0 Use ?stage=Stage querystring arguments instead of useDraftSite
429
         * @param bool $enabled toggle the use of the draft site
430
         */
431
        public function useDraftSite($enabled = true)
432
        {
433
            Deprecation::notice('5.0', 'Use ?stage=Stage querystring arguments instead of useDraftSite');
434
            if ($enabled) {
435
                $this->session()->set('readingMode', 'Stage.Stage');
436
                $this->session()->set('unsecuredDraftSite', true);
437
            } else {
438
                $this->session()->clear('readingMode');
439
                $this->session()->clear('unsecuredDraftSite');
440
            }
441
        }
442
443
        /**
444
         * @return bool
445
         */
446
        public static function get_disable_themes()
447
        {
448
            return static::$disable_themes;
449
        }
450
451
        /**
452
         * @deprecated 4.2.0:5.0.0 Use ?stage=Stage in your querystring arguments instead
453
         * @return bool
454
         */
455
        public static function get_use_draft_site()
456
        {
457
            return static::$use_draft_site;
458
        }
459
    }
460
}
461
462
/* -------------------------------------------------
463
 *
464
 * This version of FunctionalTest is for PHPUnit 5
465
 * The PHPUnit 9 version is at the top of this file
466
 *
467
 * PHPUnit_Extensions_GroupTestSuite is a class that only exists in PHPUnit 5
468
 *
469
 * -------------------------------------------------
470
 */
471
if (!class_exists(PHPUnit_Extensions_GroupTestSuite::class)) {
472
    return;
473
}
474
475
/**
476
 * SilverStripe-specific testing object designed to support functional testing of your web app.  It simulates get/post
477
 * requests, form submission, and can validate resulting HTML, looking up content by CSS selector.
478
 *
479
 * The example below shows how it works.
480
 *
481
 * <code>
482
 *   public function testMyForm() {
483
 *   // Visit a URL
484
 *   $this->get("your/url");
485
 *
486
 *   // Submit a form on the page that you get in response
487
 *   $this->submitForm("MyForm_ID", "action_dologin", array("Email" => "invalid email ^&*&^"));
488
 *
489
 *   // Validate the content that is returned
490
 *   $this->assertExactMatchBySelector("#MyForm_ID p.error", array("That email address is invalid."));
491
 *  }
492
 * </code>
493
 */
494
// Ignore multiple classes in same file
495
// @codingStandardsIgnoreStart
496
class FunctionalTest extends SapphireTest implements TestOnly
497
{
498
    // @codingStandardsIgnoreEnd
499
    /**
500
     * Set this to true on your sub-class to disable the use of themes in this test.
501
     * This can be handy for functional testing of modules without having to worry about whether a user has changed
502
     * behaviour by replacing the theme.
503
     *
504
     * @var bool
505
     */
506
    protected static $disable_themes = false;
507
508
    /**
509
     * Set this to true on your sub-class to use the draft site by default for every test in this class.
510
     *
511
     * @deprecated 4.2.0:5.0.0 Use ?stage=Stage in your ->get() querystring requests instead
512
     * @var bool
513
     */
514
    protected static $use_draft_site = false;
515
516
    /**
517
     * @var TestSession
518
     */
519
    protected $mainSession = null;
520
521
    /**
522
     * CSSContentParser for the most recently requested page.
523
     *
524
     * @var CSSContentParser
525
     */
526
    protected $cssParser = null;
527
528
    /**
529
     * If this is true, then 30x Location headers will be automatically followed.
530
     * If not, then you will have to manually call $this->mainSession->followRedirection() to follow them.
531
     * However, this will let you inspect the intermediary headers
532
     *
533
     * @var bool
534
     */
535
    protected $autoFollowRedirection = true;
536
537
    /**
538
     * Returns the {@link Session} object for this test
539
     *
540
     * @return Session
541
     */
542
    public function session()
543
    {
544
        return $this->mainSession->session();
545
    }
546
547
    protected function setUp()
548
    {
549
        parent::setUp();
550
551
        // Skip calling FunctionalTest directly.
552
        if (static::class == __CLASS__) {
0 ignored issues
show
introduced by
The condition static::class == __CLASS__ is always true.
Loading history...
553
            $this->markTestSkipped(sprintf('Skipping %s ', static::class));
554
        }
555
556
        $this->mainSession = new TestSession();
557
558
        // Disable theme, if necessary
559
        if (static::get_disable_themes()) {
560
            SSViewer::config()->update('theme_enabled', false);
561
        }
562
563
        // Flush user
564
        $this->logOut();
565
566
        // Switch to draft site, if necessary
567
        // If you rely on this you should be crafting stage-specific urls instead though.
568
        if (static::get_use_draft_site()) {
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Dev\Functio...t::get_use_draft_site() has been deprecated: 4.2.0:5.0.0 Use ?stage=Stage in your querystring arguments instead ( Ignorable by Annotation )

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

568
        if (/** @scrutinizer ignore-deprecated */ static::get_use_draft_site()) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
569
            $this->useDraftSite();
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Dev\FunctionalTest::useDraftSite() has been deprecated: 4.2.0:5.0.0 Use ?stage=Stage querystring arguments instead of useDraftSite ( Ignorable by Annotation )

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

569
            /** @scrutinizer ignore-deprecated */ $this->useDraftSite();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
570
        }
571
572
        // Unprotect the site, tests are running with the assumption it's off. They will enable it on a case-by-case
573
        // basis.
574
        BasicAuth::protect_entire_site(false);
575
576
        SecurityToken::disable();
577
    }
578
579
    protected function tearDown()
580
    {
581
        SecurityToken::enable();
582
        unset($this->mainSession);
583
        parent::tearDown();
584
    }
585
586
    /**
587
     * Run a test while mocking the base url with the provided value
588
     * @param string $url The base URL to use for this test
589
     * @param callable $callback The test to run
590
     */
591
    protected function withBaseURL($url, $callback)
592
    {
593
        $oldBase = Config::inst()->get(Director::class, 'alternate_base_url');
594
        Config::modify()->set(Director::class, 'alternate_base_url', $url);
595
        $callback($this);
596
        Config::modify()->set(Director::class, 'alternate_base_url', $oldBase);
597
    }
598
599
    /**
600
     * Run a test while mocking the base folder with the provided value
601
     * @param string $folder The base folder to use for this test
602
     * @param callable $callback The test to run
603
     */
604
    protected function withBaseFolder($folder, $callback)
605
    {
606
        $oldFolder = Config::inst()->get(Director::class, 'alternate_base_folder');
607
        Config::modify()->set(Director::class, 'alternate_base_folder', $folder);
608
        $callback($this);
609
        Config::modify()->set(Director::class, 'alternate_base_folder', $oldFolder);
610
    }
611
612
    /**
613
     * Submit a get request
614
     * @uses Director::test()
615
     *
616
     * @param string $url
617
     * @param Session $session
618
     * @param array $headers
619
     * @param array $cookies
620
     * @return HTTPResponse
621
     */
622
    public function get($url, $session = null, $headers = null, $cookies = null)
623
    {
624
        $this->cssParser = null;
625
        $response = $this->mainSession->get($url, $session, $headers, $cookies);
626
        if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) {
627
            $response = $this->mainSession->followRedirection();
628
        }
629
        return $response;
630
    }
631
632
    /**
633
     * Submit a post request
634
     *
635
     * @uses Director::test()
636
     * @param string $url
637
     * @param array $data
638
     * @param array $headers
639
     * @param Session $session
640
     * @param string $body
641
     * @param array $cookies
642
     * @return HTTPResponse
643
     */
644
    public function post($url, $data, $headers = null, $session = null, $body = null, $cookies = null)
645
    {
646
        $this->cssParser = null;
647
        $response = $this->mainSession->post($url, $data, $headers, $session, $body, $cookies);
648
        if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) {
649
            $response = $this->mainSession->followRedirection();
650
        }
651
        return $response;
652
    }
653
654
    /**
655
     * Submit the form with the given HTML ID, filling it out with the given data.
656
     * Acts on the most recent response.
657
     *
658
     * Any data parameters have to be present in the form, with exact form field name
659
     * and values, otherwise they are removed from the submission.
660
     *
661
     * Caution: Parameter names have to be formatted
662
     * as they are in the form submission, not as they are interpreted by PHP.
663
     * Wrong: array('mycheckboxvalues' => array(1 => 'one', 2 => 'two'))
664
     * Right: array('mycheckboxvalues[1]' => 'one', 'mycheckboxvalues[2]' => 'two')
665
     *
666
     * @see http://www.simpletest.org/en/form_testing_documentation.html
667
     *
668
     * @param string $formID HTML 'id' attribute of a form (loaded through a previous response)
669
     * @param string $button HTML 'name' attribute of the button (NOT the 'id' attribute)
670
     * @param array $data Map of GET/POST data.
671
     * @return HTTPResponse
672
     */
673
    public function submitForm($formID, $button = null, $data = [])
674
    {
675
        $this->cssParser = null;
676
        $response = $this->mainSession->submitForm($formID, $button, $data);
677
        if ($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) {
678
            $response = $this->mainSession->followRedirection();
679
        }
680
        return $response;
681
    }
682
683
    /**
684
     * Return the most recent content
685
     *
686
     * @return string
687
     */
688
    public function content()
689
    {
690
        return $this->mainSession->lastContent();
691
    }
692
693
    /**
694
     * Find an attribute in a SimpleXMLElement object by name.
695
     * @param SimpleXMLElement $object
696
     * @param string $attribute Name of attribute to find
697
     * @return SimpleXMLElement object of the attribute
698
     */
699
    public function findAttribute($object, $attribute)
700
    {
701
        $found = false;
702
        foreach ($object->attributes() as $a => $b) {
703
            if ($a == $attribute) {
704
                $found = $b;
705
            }
706
        }
707
        return $found;
708
    }
709
710
    /**
711
     * Return a CSSContentParser for the most recent content.
712
     *
713
     * @return CSSContentParser
714
     */
715
    public function cssParser()
716
    {
717
        if (!$this->cssParser) {
718
            $this->cssParser = new CSSContentParser($this->mainSession->lastContent());
719
        }
720
        return $this->cssParser;
721
    }
722
723
    /**
724
     * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
725
     * The given CSS selector will be applied to the HTML of the most recent page.  The content of every matching tag
726
     * will be examined. The assertion fails if one of the expectedMatches fails to appear.
727
     *
728
     * Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account.
729
     *
730
     * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
731
     * @param array|string $expectedMatches The content of at least one of the matched tags
732
     * @param string $message
733
     * @throws PHPUnit_Framework_AssertionFailedError
734
     */
735
    public function assertPartialMatchBySelector($selector, $expectedMatches, $message = null)
736
    {
737
        if (is_string($expectedMatches)) {
738
            $expectedMatches = [$expectedMatches];
739
        }
740
741
        $items = $this->cssParser()->getBySelector($selector);
742
743
        $actuals = [];
744
        if ($items) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $items of type SimpleXMLElement[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
745
            foreach ($items as $item) {
746
                $actuals[trim(preg_replace('/\s+/', ' ', (string)$item))] = true;
747
            }
748
        }
749
750
        $message = $message ?:
751
        "Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'"
752
            . implode("'\n'", $expectedMatches) . "'\n\n"
753
            . "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals ?: [])) . "'";
754
755
        foreach ($expectedMatches as $match) {
756
            $this->assertTrue(isset($actuals[$match]), $message);
757
        }
758
    }
759
760
    /**
761
     * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
762
     * The given CSS selector will be applied to the HTML of the most recent page.  The full HTML of every matching tag
763
     * will be examined. The assertion fails if one of the expectedMatches fails to appear.
764
     *
765
     * Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account.
766
     *
767
     * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
768
     * @param array|string $expectedMatches The content of *all* matching tags as an array
769
     * @param string $message
770
     * @throws PHPUnit_Framework_AssertionFailedError
771
     */
772
    public function assertExactMatchBySelector($selector, $expectedMatches, $message = null)
773
    {
774
        if (is_string($expectedMatches)) {
775
            $expectedMatches = [$expectedMatches];
776
        }
777
778
        $items = $this->cssParser()->getBySelector($selector);
779
780
        $actuals = [];
781
        if ($items) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $items of type SimpleXMLElement[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
782
            foreach ($items as $item) {
783
                $actuals[] = trim((string) preg_replace('/\s+/', ' ', (string)$item));
784
            }
785
        }
786
787
        $message = $message ?:
788
                "Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'"
789
                . implode("'\n'", $expectedMatches) . "'\n\n"
790
            . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'";
791
792
        $this->assertTrue($expectedMatches == $actuals, $message);
793
    }
794
795
    /**
796
     * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
797
     * The given CSS selector will be applied to the HTML of the most recent page.  The content of every matching tag
798
     * will be examined. The assertion fails if one of the expectedMatches fails to appear.
799
     *
800
     * Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account.
801
     *
802
     * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
803
     * @param array|string $expectedMatches The content of at least one of the matched tags
804
     * @param string $message
805
     * @throws PHPUnit_Framework_AssertionFailedError
806
     */
807
    public function assertPartialHTMLMatchBySelector($selector, $expectedMatches, $message = null)
808
    {
809
        if (is_string($expectedMatches)) {
810
            $expectedMatches = [$expectedMatches];
811
        }
812
813
        $items = $this->cssParser()->getBySelector($selector);
814
815
        $actuals = [];
816
        if ($items) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $items of type SimpleXMLElement[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
817
            /** @var SimpleXMLElement $item */
818
            foreach ($items as $item) {
819
                $actuals[$item->asXML()] = true;
820
            }
821
        }
822
823
        $message = $message ?:
824
                "Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'"
825
                . implode("'\n'", $expectedMatches) . "'\n\n"
826
            . "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals ?: [])) . "'";
827
828
        foreach ($expectedMatches as $match) {
829
            $this->assertTrue(isset($actuals[$match]), $message);
830
        }
831
    }
832
833
    /**
834
     * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
835
     * The given CSS selector will be applied to the HTML of the most recent page.  The full HTML of every matching tag
836
     * will be examined. The assertion fails if one of the expectedMatches fails to appear.
837
     *
838
     * Note: &nbsp; characters are stripped from the content; make sure that your assertions take this into account.
839
     *
840
     * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
841
     * @param array|string $expectedMatches The content of *all* matched tags as an array
842
     * @param string $message
843
     * @throws PHPUnit_Framework_AssertionFailedError
844
     */
845
    public function assertExactHTMLMatchBySelector($selector, $expectedMatches, $message = null)
846
    {
847
        $items = $this->cssParser()->getBySelector($selector);
848
849
        $actuals = [];
850
        if ($items) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $items of type SimpleXMLElement[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
851
            /** @var SimpleXMLElement $item */
852
            foreach ($items as $item) {
853
                $actuals[] = $item->asXML();
854
            }
855
        }
856
857
        $message = $message ?:
858
            "Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'"
859
            . implode("'\n'", $expectedMatches) . "'\n\n"
0 ignored issues
show
Bug introduced by
It seems like $expectedMatches can also be of type string; however, parameter $pieces of implode() does only seem to accept array, 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

859
            . implode("'\n'", /** @scrutinizer ignore-type */ $expectedMatches) . "'\n\n"
Loading history...
860
            . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'";
861
862
        $this->assertTrue($expectedMatches == $actuals, $message);
863
    }
864
865
    /**
866
     * Use the draft (stage) site for testing.
867
     * This is helpful if you're not testing publication functionality and don't want "stage management" cluttering
868
     * your test.
869
     *
870
     * @deprecated 4.2.0:5.0.0 Use ?stage=Stage querystring arguments instead of useDraftSite
871
     * @param bool $enabled toggle the use of the draft site
872
     */
873
    public function useDraftSite($enabled = true)
874
    {
875
        Deprecation::notice('5.0', 'Use ?stage=Stage querystring arguments instead of useDraftSite');
876
        if ($enabled) {
877
            $this->session()->set('readingMode', 'Stage.Stage');
878
            $this->session()->set('unsecuredDraftSite', true);
879
        } else {
880
            $this->session()->clear('readingMode');
881
            $this->session()->clear('unsecuredDraftSite');
882
        }
883
    }
884
885
    /**
886
     * @return bool
887
     */
888
    public static function get_disable_themes()
889
    {
890
        return static::$disable_themes;
891
    }
892
893
    /**
894
     * @deprecated 4.2.0:5.0.0 Use ?stage=Stage in your querystring arguments instead
895
     * @return bool
896
     */
897
    public static function get_use_draft_site()
898
    {
899
        return static::$use_draft_site;
900
    }
901
}
902