Passed
Push — fix-8832 ( 2eb5fa )
by Sam
07:48
created

SSViewerTest::testUpInLoop()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 86
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 54
nc 1
nop 0
dl 0
loc 86
rs 9.0036
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\View\Tests;
4
5
use Exception;
6
use InvalidArgumentException;
7
use PHPUnit_Framework_MockObject_MockObject;
8
use Silverstripe\Assets\Dev\TestAssetStore;
9
use SilverStripe\Control\ContentNegotiator;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Control\HTTPResponse;
13
use SilverStripe\Core\Convert;
14
use SilverStripe\Core\Injector\Injector;
15
use SilverStripe\Dev\SapphireTest;
16
use SilverStripe\i18n\i18n;
17
use SilverStripe\ORM\ArrayList;
18
use SilverStripe\ORM\DataObject;
19
use SilverStripe\ORM\FieldType\DBField;
20
use SilverStripe\ORM\PaginatedList;
21
use SilverStripe\Security\Permission;
22
use SilverStripe\Security\Security;
23
use SilverStripe\Security\SecurityToken;
24
use SilverStripe\View\ArrayData;
25
use SilverStripe\View\Requirements;
26
use SilverStripe\View\Requirements_Backend;
27
use SilverStripe\View\Requirements_Minifier;
28
use SilverStripe\View\SSTemplateParser;
29
use SilverStripe\View\SSViewer;
30
use SilverStripe\View\SSViewer_FromString;
31
use SilverStripe\View\Tests\SSViewerTest\SSViewerTestModel;
32
use SilverStripe\View\Tests\SSViewerTest\SSViewerTestModelController;
33
use SilverStripe\View\Tests\SSViewerTest\TestViewableData;
34
use SilverStripe\View\ViewableData;
35
36
/**
37
 * @skipUpgrade
38
 */
39
class SSViewerTest extends SapphireTest
40
{
41
42
    /**
43
     * Backup of $_SERVER global
44
     *
45
     * @var array
46
     */
47
    protected $oldServer = array();
48
49
    protected static $extra_dataobjects = array(
50
        SSViewerTest\TestObject::class,
51
    );
52
53
    protected function setUp()
54
    {
55
        parent::setUp();
56
        SSViewer::config()->update('source_file_comments', false);
57
        SSViewer_FromString::config()->update('cache_template', false);
58
        TestAssetStore::activate('SSViewerTest');
59
        $this->oldServer = $_SERVER;
60
    }
61
62
    protected function tearDown()
63
    {
64
        $_SERVER = $this->oldServer;
65
        TestAssetStore::reset();
66
        parent::tearDown();
67
    }
68
69
    /**
70
     * Tests for {@link Config::inst()->get('SSViewer', 'theme')} for different behaviour
71
     * of user defined themes via {@link SiteConfig} and default theme
72
     * when no user themes are defined.
73
     */
74
    public function testCurrentTheme()
75
    {
76
        SSViewer::config()->update('theme', 'mytheme');
77
        $this->assertEquals(
78
            'mytheme',
79
            SSViewer::config()->uninherited('theme'),
80
            'Current theme is the default - user has not defined one'
81
        );
82
    }
83
84
    /**
85
     * Tests for themes helper functions, ensuring they behave as defined in the RFC at
86
     * https://github.com/silverstripe/silverstripe-framework/issues/5604
87
     */
88
    public function testThemesHelpers()
89
    {
90
        // Test set_themes()
91
        SSViewer::set_themes(['mytheme', '$default']);
92
        $this->assertEquals(['mytheme', '$default'], SSViewer::get_themes());
93
94
        // Ensure add_themes() prepends
95
        SSViewer::add_themes(['my_more_important_theme']);
96
        $this->assertEquals(['my_more_important_theme', 'mytheme', '$default'], SSViewer::get_themes());
97
98
        // Ensure add_themes() on theme already in cascade promotes it to the top
99
        SSViewer::add_themes(['mytheme']);
100
        $this->assertEquals(['mytheme', 'my_more_important_theme', '$default'], SSViewer::get_themes());
101
    }
102
103
    /**
104
     * Test that a template without a <head> tag still renders.
105
     */
106
    public function testTemplateWithoutHeadRenders()
107
    {
108
        $data = new ArrayData([ 'Var' => 'var value' ]);
109
        $result = $data->renderWith("SSViewerTestPartialTemplate");
110
        $this->assertEquals('Test partial template: var value', trim(preg_replace("/<!--.*-->/U", '', $result)));
111
    }
112
113
    /**
114
     * Ensure global methods aren't executed
115
     */
116
    public function testTemplateExecution()
117
    {
118
        $data = new ArrayData([ 'Var' => 'phpinfo' ]);
119
        $result = $data->renderWith("SSViewerTestPartialTemplate");
120
        $this->assertEquals('Test partial template: phpinfo', trim(preg_replace("/<!--.*-->/U", '', $result)));
121
    }
122
123
    public function testIncludeScopeInheritance()
124
    {
125
        $data = $this->getScopeInheritanceTestData();
126
        $expected = array(
127
        'Item 1 - First-ODD top:Item 1',
128
        'Item 2 - EVEN top:Item 2',
129
        'Item 3 - ODD top:Item 3',
130
        'Item 4 - EVEN top:Item 4',
131
        'Item 5 - ODD top:Item 5',
132
        'Item 6 - Last-EVEN top:Item 6',
133
        );
134
135
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritance');
136
        $this->assertExpectedStrings($result, $expected);
137
138
        // reset results for the tests that include arguments (the title is passed as an arg)
139
        $expected = array(
140
        'Item 1 _ Item 1 - First-ODD top:Item 1',
141
        'Item 2 _ Item 2 - EVEN top:Item 2',
142
        'Item 3 _ Item 3 - ODD top:Item 3',
143
        'Item 4 _ Item 4 - EVEN top:Item 4',
144
        'Item 5 _ Item 5 - ODD top:Item 5',
145
        'Item 6 _ Item 6 - Last-EVEN top:Item 6',
146
        );
147
148
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
149
        $this->assertExpectedStrings($result, $expected);
150
    }
151
152
    public function testIncludeTruthyness()
153
    {
154
        $data = new ArrayData([
155
            'Title' => 'TruthyTest',
156
            'Items' => new ArrayList([
157
                new ArrayData(['Title' => 'Item 1']),
158
                new ArrayData(['Title' => '']),
159
                new ArrayData(['Title' => true]),
160
                new ArrayData(['Title' => false]),
161
                new ArrayData(['Title' => null]),
162
                new ArrayData(['Title' => 0]),
163
                new ArrayData(['Title' => 7])
164
            ])
165
        ]);
166
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
167
168
        // We should not end up with empty values appearing as empty
169
        $expected = [
170
            'Item 1 _ Item 1 - First-ODD top:Item 1',
171
            'Untitled - EVEN top:',
172
            '1 _ 1 - ODD top:1',
173
            'Untitled - EVEN top:',
174
            'Untitled - ODD top:',
175
            'Untitled - EVEN top:0',
176
            '7 _ 7 - Last-ODD top:7',
177
        ];
178
        $this->assertExpectedStrings($result, $expected);
179
    }
180
181
    private function getScopeInheritanceTestData()
182
    {
183
        return new ArrayData([
184
            'Title' => 'TopTitleValue',
185
            'Items' => new ArrayList([
186
                new ArrayData(['Title' => 'Item 1']),
187
                new ArrayData(['Title' => 'Item 2']),
188
                new ArrayData(['Title' => 'Item 3']),
189
                new ArrayData(['Title' => 'Item 4']),
190
                new ArrayData(['Title' => 'Item 5']),
191
                new ArrayData(['Title' => 'Item 6'])
192
            ])
193
        ]);
194
    }
195
196
    private function assertExpectedStrings($result, $expected)
197
    {
198
        foreach ($expected as $expectedStr) {
199
            $this->assertTrue(
200
                (boolean) preg_match("/{$expectedStr}/", $result),
201
                "Didn't find '{$expectedStr}' in:\n{$result}"
202
            );
203
        }
204
    }
205
206
    /**
207
     * Small helper to render templates from strings
208
     *
209
     * @param  string $templateString
210
     * @param  null   $data
211
     * @param  bool   $cacheTemplate
212
     * @return string
213
     */
214
    public function render($templateString, $data = null, $cacheTemplate = false)
215
    {
216
        $t = SSViewer::fromString($templateString, $cacheTemplate);
217
        if (!$data) {
218
            $data = new SSViewerTest\TestFixture();
219
        }
220
        return trim('' . $t->process($data));
221
    }
222
223
    public function testRequirements()
224
    {
225
        /** @var Requirements_Backend|PHPUnit_Framework_MockObject_MockObject $requirements */
226
        $requirements = $this
227
            ->getMockBuilder(Requirements_Backend::class)
228
            ->setMethods(array("javascript", "css"))
229
            ->getMock();
230
        $jsFile = FRAMEWORK_DIR . '/tests/forms/a.js';
231
        $cssFile = FRAMEWORK_DIR . '/tests/forms/a.js';
232
233
        $requirements->expects($this->once())->method('javascript')->with($jsFile);
234
        $requirements->expects($this->once())->method('css')->with($cssFile);
235
236
        $origReq = Requirements::backend();
237
        Requirements::set_backend($requirements);
238
        $template = $this->render(
239
            "<% require javascript($jsFile) %>
240
		<% require css($cssFile) %>"
241
        );
242
        Requirements::set_backend($origReq);
243
244
        $this->assertFalse((bool)trim($template), "Should be no content in this return.");
245
    }
246
247
    public function testRequirementsCombine()
248
    {
249
        /** @var Requirements_Backend $testBackend */
250
        $testBackend = Injector::inst()->create(Requirements_Backend::class);
251
        $testBackend->setSuffixRequirements(false);
252
        $testBackend->setCombinedFilesEnabled(true);
253
254
        $jsFile = $this->getCurrentRelativePath() . '/SSViewerTest/javascript/bad.js';
255
        $jsFileContents = file_get_contents(BASE_PATH . '/' . $jsFile);
256
        $testBackend->combineFiles('testRequirementsCombine.js', array($jsFile));
257
258
        // secondly, make sure that requirements is generated, even though minification failed
259
        $testBackend->processCombinedFiles();
260
        $js = array_keys($testBackend->getJavascript());
261
        $combinedTestFilePath = Director::publicFolder() . reset($js);
262
        $this->assertContains('_combinedfiles/testRequirementsCombine-4c0e97a.js', $combinedTestFilePath);
263
264
        // and make sure the combined content matches the input content, i.e. no loss of functionality
265
        if (!file_exists($combinedTestFilePath)) {
266
            $this->fail('No combined file was created at expected path: ' . $combinedTestFilePath);
267
        }
268
        $combinedTestFileContents = file_get_contents($combinedTestFilePath);
269
        $this->assertContains($jsFileContents, $combinedTestFileContents);
270
    }
271
272
    public function testRequirementsMinification()
273
    {
274
        /** @var Requirements_Backend $testBackend */
275
        $testBackend = Injector::inst()->create(Requirements_Backend::class);
276
        $testBackend->setSuffixRequirements(false);
277
        $testBackend->setMinifyCombinedFiles(true);
278
        $testBackend->setCombinedFilesEnabled(true);
279
280
        $testFile = $this->getCurrentRelativePath() . '/SSViewerTest/javascript/RequirementsTest_a.js';
281
        $testFileContent = file_get_contents($testFile);
282
283
        $mockMinifier = $this->getMockBuilder(Requirements_Minifier::class)
284
        ->setMethods(['minify'])
285
        ->getMock();
286
287
        $mockMinifier->expects($this->once())
288
        ->method('minify')
289
        ->with(
290
            $testFileContent,
291
            'js',
292
            $testFile
293
        );
294
        $testBackend->setMinifier($mockMinifier);
295
        $testBackend->combineFiles('testRequirementsMinified.js', array($testFile));
296
        $testBackend->processCombinedFiles();
297
298
        $testBackend->setMinifyCombinedFiles(false);
299
        $mockMinifier->expects($this->never())
300
        ->method('minify');
301
        $testBackend->processCombinedFiles();
302
303
        $this->expectException(Exception::class);
304
        $this->expectExceptionMessageRegExp('/^Cannot minify files without a minification service defined./');
305
306
        $testBackend->setMinifyCombinedFiles(true);
307
        $testBackend->setMinifier(null);
308
        $testBackend->processCombinedFiles();
309
    }
310
311
312
313
    public function testComments()
314
    {
315
        $input = <<<SS
316
This is my template<%-- this is a comment --%>This is some content<%-- this is another comment --%>Final content
317
<%-- Alone multi
318
	line comment --%>
319
Some more content
320
Mixing content and <%-- multi
321
	line comment --%> Final final
322
content
323
SS;
324
        $output = $this->render($input);
325
        $shouldbe = <<<SS
326
This is my templateThis is some contentFinal content
327
328
Some more content
329
Mixing content and  Final final
330
content
331
SS;
332
        $this->assertEquals($shouldbe, $output);
333
    }
334
335
    public function testBasicText()
336
    {
337
        $this->assertEquals('"', $this->render('"'), 'Double-quotes are left alone');
338
        $this->assertEquals("'", $this->render("'"), 'Single-quotes are left alone');
339
        $this->assertEquals('A', $this->render('\\A'), 'Escaped characters are unescaped');
340
        $this->assertEquals('\\A', $this->render('\\\\A'), 'Escaped back-slashed are correctly unescaped');
341
    }
342
343
    public function testBasicInjection()
344
    {
345
        $this->assertEquals('[out:Test]', $this->render('$Test'), 'Basic stand-alone injection');
346
        $this->assertEquals('[out:Test]', $this->render('{$Test}'), 'Basic stand-alone wrapped injection');
347
        $this->assertEquals('A[out:Test]!', $this->render('A$Test!'), 'Basic surrounded injection');
348
        $this->assertEquals('A[out:Test]B', $this->render('A{$Test}B'), 'Basic surrounded wrapped injection');
349
350
        $this->assertEquals('A$B', $this->render('A\\$B'), 'No injection as $ escaped');
351
        $this->assertEquals('A$ B', $this->render('A$ B'), 'No injection as $ not followed by word character');
352
        $this->assertEquals('A{$ B', $this->render('A{$ B'), 'No injection as {$ not followed by word character');
353
354
        $this->assertEquals('{$Test}', $this->render('{\\$Test}'), 'Escapes can be used to avoid injection');
355
        $this->assertEquals(
356
            '{\\[out:Test]}',
357
            $this->render('{\\\\$Test}'),
358
            'Escapes before injections are correctly unescaped'
359
        );
360
    }
361
362
363
    public function testGlobalVariableCalls()
364
    {
365
        $this->assertEquals('automatic', $this->render('$SSViewerTest_GlobalAutomatic'));
366
        $this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedByString'));
367
        $this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedInArray'));
368
    }
369
370
    public function testGlobalVariableCallsWithArguments()
371
    {
372
        $this->assertEquals('zz', $this->render('$SSViewerTest_GlobalThatTakesArguments'));
373
        $this->assertEquals('zFooz', $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo")'));
374
        $this->assertEquals(
375
            'zFoo:Bar:Bazz',
376
            $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo", "Bar", "Baz")')
377
        );
378
        $this->assertEquals(
379
            'zreferencez',
380
            $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalReferencedByString)')
381
        );
382
    }
383
384
    public function testGlobalVariablesAreEscaped()
385
    {
386
        $this->assertEquals('<div></div>', $this->render('$SSViewerTest_GlobalHTMLFragment'));
387
        $this->assertEquals('&lt;div&gt;&lt;/div&gt;', $this->render('$SSViewerTest_GlobalHTMLEscaped'));
388
389
        $this->assertEquals(
390
            'z<div></div>z',
391
            $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLFragment)')
392
        );
393
        $this->assertEquals(
394
            'z&lt;div&gt;&lt;/div&gt;z',
395
            $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLEscaped)')
396
        );
397
    }
398
399
    public function testCoreGlobalVariableCalls()
400
    {
401
        $this->assertEquals(
402
            Director::absoluteBaseURL(),
403
            $this->render('{$absoluteBaseURL}'),
404
            'Director::absoluteBaseURL can be called from within template'
405
        );
406
        $this->assertEquals(
407
            Director::absoluteBaseURL(),
408
            $this->render('{$AbsoluteBaseURL}'),
409
            'Upper-case %AbsoluteBaseURL can be called from within template'
410
        );
411
412
        $this->assertEquals(
413
            Director::is_ajax(),
414
            $this->render('{$isAjax}'),
415
            'All variations of is_ajax result in the correct call'
416
        );
417
        $this->assertEquals(
418
            Director::is_ajax(),
419
            $this->render('{$IsAjax}'),
420
            'All variations of is_ajax result in the correct call'
421
        );
422
        $this->assertEquals(
423
            Director::is_ajax(),
424
            $this->render('{$is_ajax}'),
425
            'All variations of is_ajax result in the correct call'
426
        );
427
        $this->assertEquals(
428
            Director::is_ajax(),
429
            $this->render('{$Is_ajax}'),
430
            'All variations of is_ajax result in the correct call'
431
        );
432
433
        $this->assertEquals(
434
            i18n::get_locale(),
435
            $this->render('{$i18nLocale}'),
436
            'i18n template functions result correct result'
437
        );
438
        $this->assertEquals(
439
            i18n::get_locale(),
440
            $this->render('{$get_locale}'),
441
            'i18n template functions result correct result'
442
        );
443
444
        $this->assertEquals(
445
            (string)Security::getCurrentUser(),
446
            $this->render('{$CurrentMember}'),
447
            'Member template functions result correct result'
448
        );
449
        $this->assertEquals(
450
            (string)Security::getCurrentUser(),
451
            $this->render('{$CurrentUser}'),
452
            'Member template functions result correct result'
453
        );
454
        $this->assertEquals(
455
            (string)Security::getCurrentUser(),
456
            $this->render('{$currentMember}'),
457
            'Member template functions result correct result'
458
        );
459
        $this->assertEquals(
460
            (string)Security::getCurrentUser(),
461
            $this->render('{$currentUser}'),
462
            'Member template functions result correct result'
463
        );
464
465
        $this->assertEquals(
466
            SecurityToken::getSecurityID(),
467
            $this->render('{$getSecurityID}'),
468
            'SecurityToken template functions result correct result'
469
        );
470
        $this->assertEquals(
471
            SecurityToken::getSecurityID(),
472
            $this->render('{$SecurityID}'),
473
            'SecurityToken template functions result correct result'
474
        );
475
476
        $this->assertEquals(
477
            Permission::check("ADMIN"),
478
            (bool)$this->render('{$HasPerm(\'ADMIN\')}'),
479
            'Permissions template functions result correct result'
480
        );
481
        $this->assertEquals(
482
            Permission::check("ADMIN"),
483
            (bool)$this->render('{$hasPerm(\'ADMIN\')}'),
484
            'Permissions template functions result correct result'
485
        );
486
    }
487
488
    public function testNonFieldCastingHelpersNotUsedInHasValue()
489
    {
490
        // check if Link without $ in front of variable
491
        $result = $this->render(
492
            'A<% if Link %>$Link<% end_if %>B',
493
            new SSViewerTest\TestObject()
494
        );
495
        $this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if Link %>');
496
497
        // check if Link with $ in front of variable
498
        $result = $this->render(
499
            'A<% if $Link %>$Link<% end_if %>B',
500
            new SSViewerTest\TestObject()
501
        );
502
        $this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if $Link %>');
503
    }
504
505
    public function testLocalFunctionsTakePriorityOverGlobals()
506
    {
507
        $data = new ArrayData([
508
            'Page' => new SSViewerTest\TestObject()
509
        ]);
510
511
        //call method with lots of arguments
512
        $result = $this->render(
513
            '<% with Page %>$lotsOfArguments11("a","b","c","d","e","f","g","h","i","j","k")<% end_with %>',
514
            $data
515
        );
516
        $this->assertEquals("abcdefghijk", $result, "public function can accept up to 11 arguments");
517
518
        //call method that does not exist
519
        $result = $this->render('<% with Page %><% if IDoNotExist %>hello<% end_if %><% end_with %>', $data);
520
        $this->assertEquals("", $result, "Method does not exist - empty result");
521
522
        //call if that does not exist
523
        $result = $this->render('<% with Page %>$IDoNotExist("hello")<% end_with %>', $data);
524
        $this->assertEquals("", $result, "Method does not exist - empty result");
525
526
        //call method with same name as a global method (local call should take priority)
527
        $result = $this->render('<% with Page %>$absoluteBaseURL<% end_with %>', $data);
528
        $this->assertEquals(
529
            "testLocalFunctionPriorityCalled",
530
            $result,
531
            "Local Object's public function called. Did not return the actual baseURL of the current site"
532
        );
533
    }
534
535
    public function testCurrentScopeLoopWith()
536
    {
537
        // Data to run the loop tests on - one sequence of three items, each with a subitem
538
        $data = new ArrayData([
539
            'Foo' => new ArrayList([
540
                'Subocean' => new ArrayData([
541
                    'Name' => 'Higher'
542
                ]),
543
                new ArrayData([
544
                    'Sub' => new ArrayData([
545
                        'Name' => 'SubKid1'
546
                    ])
547
                ]),
548
                new ArrayData([
549
                    'Sub' => new ArrayData([
550
                        'Name' => 'SubKid2'
551
                    ])
552
                ]),
553
                new SSViewerTest\TestObject('Number6')
554
            ])
555
        ]);
556
557
        $result = $this->render(
558
            '<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',
559
            $data
560
        );
561
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop works");
562
563
        $result = $this->render(
564
            '<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',
565
            $data
566
        );
567
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop works");
568
569
        $result = $this->render('<% with Foo %>$Count<% end_with %>', $data);
570
        $this->assertEquals("4", $result, "4 items in the DataObjectSet");
571
572
        $result = $this->render(
573
            '<% with Foo %><% loop Up.Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
574
            . '<% end_if %><% end_loop %><% end_with %>',
575
            $data
576
        );
577
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop in with Up.Foo scope works");
578
579
        $result = $this->render(
580
            '<% with Foo %><% loop %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
581
            . '<% end_if %><% end_loop %><% end_with %>',
582
            $data
583
        );
584
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop in current scope works");
585
    }
586
587
    public function testObjectDotArguments()
588
    {
589
        $this->assertEquals(
590
            '[out:TestObject.methodWithOneArgument(one)]
591
				[out:TestObject.methodWithTwoArguments(one,two)]
592
				[out:TestMethod(Arg1,Arg2).Bar.Val]
593
				[out:TestMethod(Arg1,Arg2).Bar]
594
				[out:TestMethod(Arg1,Arg2)]
595
				[out:TestMethod(Arg1).Bar.Val]
596
				[out:TestMethod(Arg1).Bar]
597
				[out:TestMethod(Arg1)]',
598
            $this->render(
599
                '$TestObject.methodWithOneArgument(one)
600
				$TestObject.methodWithTwoArguments(one,two)
601
				$TestMethod(Arg1, Arg2).Bar.Val
602
				$TestMethod(Arg1, Arg2).Bar
603
				$TestMethod(Arg1, Arg2)
604
				$TestMethod(Arg1).Bar.Val
605
				$TestMethod(Arg1).Bar
606
				$TestMethod(Arg1)'
607
            )
608
        );
609
    }
610
611
    public function testEscapedArguments()
612
    {
613
        $this->assertEquals(
614
            '[out:Foo(Arg1,Arg2).Bar.Val].Suffix
615
				[out:Foo(Arg1,Arg2).Val]_Suffix
616
				[out:Foo(Arg1,Arg2)]/Suffix
617
				[out:Foo(Arg1).Bar.Val]textSuffix
618
				[out:Foo(Arg1).Bar].Suffix
619
				[out:Foo(Arg1)].Suffix
620
				[out:Foo.Bar.Val].Suffix
621
				[out:Foo.Bar].Suffix
622
				[out:Foo].Suffix',
623
            $this->render(
624
                '{$Foo(Arg1, Arg2).Bar.Val}.Suffix
625
				{$Foo(Arg1, Arg2).Val}_Suffix
626
				{$Foo(Arg1, Arg2)}/Suffix
627
				{$Foo(Arg1).Bar.Val}textSuffix
628
				{$Foo(Arg1).Bar}.Suffix
629
				{$Foo(Arg1)}.Suffix
630
				{$Foo.Bar.Val}.Suffix
631
				{$Foo.Bar}.Suffix
632
				{$Foo}.Suffix'
633
            )
634
        );
635
    }
636
637
    public function testLoopWhitespace()
638
    {
639
        $this->assertEquals(
640
            'before[out:SingleItem.Test]after
641
				beforeTestafter',
642
            $this->render(
643
                'before<% loop SingleItem %>$Test<% end_loop %>after
644
				before<% loop SingleItem %>Test<% end_loop %>after'
645
            )
646
        );
647
648
        // The control tags are removed from the output, but no whitespace
649
        // This is a quirk that could be changed, but included in the test to make the current
650
        // behaviour explicit
651
        $this->assertEquals(
652
            'before
653
654
[out:SingleItem.ItemOnItsOwnLine]
655
656
after',
657
            $this->render(
658
                'before
659
<% loop SingleItem %>
660
$ItemOnItsOwnLine
661
<% end_loop %>
662
after'
663
            )
664
        );
665
666
        // The whitespace within the control tags is preserve in a loop
667
        // This is a quirk that could be changed, but included in the test to make the current
668
        // behaviour explicit
669
        $this->assertEquals(
670
            'before
671
672
[out:Loop3.ItemOnItsOwnLine]
673
674
[out:Loop3.ItemOnItsOwnLine]
675
676
[out:Loop3.ItemOnItsOwnLine]
677
678
after',
679
            $this->render(
680
                'before
681
<% loop Loop3 %>
682
$ItemOnItsOwnLine
683
<% end_loop %>
684
after'
685
            )
686
        );
687
    }
688
689
    public function typePreservationDataProvider()
690
    {
691
        return [
692
            // Null
693
            ['NULL:', 'null'],
694
            ['NULL:', 'NULL'],
695
            // Booleans
696
            ['boolean:1', 'true'],
697
            ['boolean:1', 'TRUE'],
698
            ['boolean:', 'false'],
699
            ['boolean:', 'FALSE'],
700
            // Strings which may look like booleans/null to the parser
701
            ['string:nullish', 'nullish'],
702
            ['string:notnull', 'notnull'],
703
            ['string:truethy', 'truethy'],
704
            ['string:untrue', 'untrue'],
705
            ['string:falsey', 'falsey'],
706
            // Integers
707
            ['integer:0', '0'],
708
            ['integer:1', '1'],
709
            ['integer:15', '15'],
710
            ['integer:-15', '-15'],
711
            // Octal integers
712
            ['integer:83', '0123'],
713
            ['integer:-83', '-0123'],
714
            // Hexadecimal integers
715
            ['integer:26', '0x1A'],
716
            ['integer:-26', '-0x1A'],
717
            // Binary integers
718
            ['integer:255', '0b11111111'],
719
            ['integer:-255', '-0b11111111'],
720
            // Floats (aka doubles)
721
            ['double:0', '0.0'],
722
            ['double:1', '1.0'],
723
            ['double:15.25', '15.25'],
724
            ['double:-15.25', '-15.25'],
725
            ['double:1200', '1.2e3'],
726
            ['double:-1200', '-1.2e3'],
727
            ['double:0.07', '7E-2'],
728
            ['double:-0.07', '-7E-2'],
729
            // Explicitly quoted strings
730
            ['string:0', '"0"'],
731
            ['string:1', '\'1\''],
732
            ['string:foobar', '"foobar"'],
733
            ['string:foo bar baz', '"foo bar baz"'],
734
            // Implicit strings
735
            ['string:foobar', 'foobar'],
736
            ['string:foo bar baz', 'foo bar baz']
737
        ];
738
    }
739
740
    /**
741
     * @dataProvider typePreservationDataProvider
742
     */
743
    public function testTypesArePreserved($expected, $templateArg)
744
    {
745
        $data = new ArrayData([
746
            'Test' => new TestViewableData()
747
        ]);
748
749
        $this->assertEquals($expected, $this->render("\$Test.Type({$templateArg})", $data));
750
    }
751
752
    /**
753
     * @dataProvider typePreservationDataProvider
754
     */
755
    public function testTypesArePreservedAsIncludeArguments($expected, $templateArg)
756
    {
757
        $data = new ArrayData([
758
            'Test' => new TestViewableData()
759
        ]);
760
761
        $this->assertEquals(
762
            $expected,
763
            $this->render("<% include SSViewerTestTypePreservation Argument={$templateArg} %>", $data)
764
        );
765
    }
766
767
    public function testTypePreservationInConditionals()
768
    {
769
        $data = new ArrayData([
770
            'Test' => new TestViewableData()
771
        ]);
772
773
        // Types in conditionals
774
        $this->assertEquals('pass', $this->render('<% if true %>pass<% else %>fail<% end_if %>', $data));
775
        $this->assertEquals('pass', $this->render('<% if false %>fail<% else %>pass<% end_if %>', $data));
776
        $this->assertEquals('pass', $this->render('<% if 1 %>pass<% else %>fail<% end_if %>', $data));
777
        $this->assertEquals('pass', $this->render('<% if 0 %>fail<% else %>pass<% end_if %>', $data));
778
    }
779
780
    public function testControls()
781
    {
782
        // Single item controls
783
        $this->assertEquals(
784
            'a[out:Foo.Bar.Item]b
785
				[out:Foo.Bar(Arg1).Item]
786
				[out:Foo(Arg1).Item]
787
				[out:Foo(Arg1,Arg2).Item]
788
				[out:Foo(Arg1,Arg2,Arg3).Item]',
789
            $this->render(
790
                '<% with Foo.Bar %>a{$Item}b<% end_with %>
791
				<% with Foo.Bar(Arg1) %>$Item<% end_with %>
792
				<% with Foo(Arg1) %>$Item<% end_with %>
793
				<% with Foo(Arg1, Arg2) %>$Item<% end_with %>
794
				<% with Foo(Arg1, Arg2, Arg3) %>$Item<% end_with %>'
795
            )
796
        );
797
798
        // Loop controls
799
        $this->assertEquals(
800
            'a[out:Foo.Loop2.Item]ba[out:Foo.Loop2.Item]b',
801
            $this->render('<% loop Foo.Loop2 %>a{$Item}b<% end_loop %>')
802
        );
803
804
        $this->assertEquals(
805
            '[out:Foo.Loop2(Arg1).Item][out:Foo.Loop2(Arg1).Item]',
806
            $this->render('<% loop Foo.Loop2(Arg1) %>$Item<% end_loop %>')
807
        );
808
809
        $this->assertEquals(
810
            '[out:Loop2(Arg1).Item][out:Loop2(Arg1).Item]',
811
            $this->render('<% loop Loop2(Arg1) %>$Item<% end_loop %>')
812
        );
813
814
        $this->assertEquals(
815
            '[out:Loop2(Arg1,Arg2).Item][out:Loop2(Arg1,Arg2).Item]',
816
            $this->render('<% loop Loop2(Arg1, Arg2) %>$Item<% end_loop %>')
817
        );
818
819
        $this->assertEquals(
820
            '[out:Loop2(Arg1,Arg2,Arg3).Item][out:Loop2(Arg1,Arg2,Arg3).Item]',
821
            $this->render('<% loop Loop2(Arg1, Arg2, Arg3) %>$Item<% end_loop %>')
822
        );
823
    }
824
825
    public function testIfBlocks()
826
    {
827
        // Basic test
828
        $this->assertEquals(
829
            'AC',
830
            $this->render('A<% if NotSet %>B$NotSet<% end_if %>C')
831
        );
832
833
        // Nested test
834
        $this->assertEquals(
835
            'AB1C',
836
            $this->render('A<% if IsSet %>B$NotSet<% if IsSet %>1<% else %>2<% end_if %><% end_if %>C')
837
        );
838
839
        // else_if
840
        $this->assertEquals(
841
            'ACD',
842
            $this->render('A<% if NotSet %>B<% else_if IsSet %>C<% end_if %>D')
843
        );
844
        $this->assertEquals(
845
            'AD',
846
            $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% end_if %>D')
847
        );
848
        $this->assertEquals(
849
            'ADE',
850
            $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E')
851
        );
852
853
        $this->assertEquals(
854
            'ADE',
855
            $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E')
856
        );
857
858
        // Dot syntax
859
        $this->assertEquals(
860
            'ACD',
861
            $this->render('A<% if Foo.NotSet %>B<% else_if Foo.IsSet %>C<% end_if %>D')
862
        );
863
        $this->assertEquals(
864
            'ACD',
865
            $this->render('A<% if Foo.Bar.NotSet %>B<% else_if Foo.Bar.IsSet %>C<% end_if %>D')
866
        );
867
868
        // Params
869
        $this->assertEquals(
870
            'ACD',
871
            $this->render('A<% if NotSet(Param) %>B<% else %>C<% end_if %>D')
872
        );
873
        $this->assertEquals(
874
            'ABD',
875
            $this->render('A<% if IsSet(Param) %>B<% else %>C<% end_if %>D')
876
        );
877
878
        // Negation
879
        $this->assertEquals(
880
            'AC',
881
            $this->render('A<% if not IsSet %>B<% end_if %>C')
882
        );
883
        $this->assertEquals(
884
            'ABC',
885
            $this->render('A<% if not NotSet %>B<% end_if %>C')
886
        );
887
888
        // Or
889
        $this->assertEquals(
890
            'ABD',
891
            $this->render('A<% if IsSet || NotSet %>B<% else_if A %>C<% end_if %>D')
892
        );
893
        $this->assertEquals(
894
            'ACD',
895
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet %>C<% end_if %>D')
896
        );
897
        $this->assertEquals(
898
            'AD',
899
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet3 %>C<% end_if %>D')
900
        );
901
        $this->assertEquals(
902
            'ACD',
903
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet || NotSet %>C<% end_if %>D')
904
        );
905
        $this->assertEquals(
906
            'AD',
907
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet2 || NotSet3 %>C<% end_if %>D')
908
        );
909
910
        // Negated Or
911
        $this->assertEquals(
912
            'ACD',
913
            $this->render('A<% if not IsSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D')
914
        );
915
        $this->assertEquals(
916
            'ABD',
917
            $this->render('A<% if not NotSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D')
918
        );
919
        $this->assertEquals(
920
            'ABD',
921
            $this->render('A<% if NotSet || not AlsoNotSet %>B<% else_if A %>C<% end_if %>D')
922
        );
923
924
        // And
925
        $this->assertEquals(
926
            'ABD',
927
            $this->render('A<% if IsSet && AlsoSet %>B<% else_if A %>C<% end_if %>D')
928
        );
929
        $this->assertEquals(
930
            'ACD',
931
            $this->render('A<% if IsSet && NotSet %>B<% else_if IsSet %>C<% end_if %>D')
932
        );
933
        $this->assertEquals(
934
            'AD',
935
            $this->render('A<% if NotSet && NotSet2 %>B<% else_if NotSet3 %>C<% end_if %>D')
936
        );
937
        $this->assertEquals(
938
            'ACD',
939
            $this->render('A<% if IsSet && NotSet %>B<% else_if IsSet && AlsoSet %>C<% end_if %>D')
940
        );
941
        $this->assertEquals(
942
            'AD',
943
            $this->render('A<% if NotSet && NotSet2 %>B<% else_if IsSet && NotSet3 %>C<% end_if %>D')
944
        );
945
946
        // Equality
947
        $this->assertEquals(
948
            'ABC',
949
            $this->render('A<% if RawVal == RawVal %>B<% end_if %>C')
950
        );
951
        $this->assertEquals(
952
            'ACD',
953
            $this->render('A<% if Right == Wrong %>B<% else_if RawVal == RawVal %>C<% end_if %>D')
954
        );
955
        $this->assertEquals(
956
            'ABC',
957
            $this->render('A<% if Right != Wrong %>B<% end_if %>C')
958
        );
959
        $this->assertEquals(
960
            'AD',
961
            $this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% end_if %>D')
962
        );
963
964
        // test inequalities with simple numbers
965
        $this->assertEquals('ABD', $this->render('A<% if 5 > 3 %>B<% else %>C<% end_if %>D'));
966
        $this->assertEquals('ABD', $this->render('A<% if 5 >= 3 %>B<% else %>C<% end_if %>D'));
967
        $this->assertEquals('ACD', $this->render('A<% if 3 > 5 %>B<% else %>C<% end_if %>D'));
968
        $this->assertEquals('ACD', $this->render('A<% if 3 >= 5 %>B<% else %>C<% end_if %>D'));
969
970
        $this->assertEquals('ABD', $this->render('A<% if 3 < 5 %>B<% else %>C<% end_if %>D'));
971
        $this->assertEquals('ABD', $this->render('A<% if 3 <= 5 %>B<% else %>C<% end_if %>D'));
972
        $this->assertEquals('ACD', $this->render('A<% if 5 < 3 %>B<% else %>C<% end_if %>D'));
973
        $this->assertEquals('ACD', $this->render('A<% if 5 <= 3 %>B<% else %>C<% end_if %>D'));
974
975
        $this->assertEquals('ABD', $this->render('A<% if 4 <= 4 %>B<% else %>C<% end_if %>D'));
976
        $this->assertEquals('ABD', $this->render('A<% if 4 >= 4 %>B<% else %>C<% end_if %>D'));
977
        $this->assertEquals('ACD', $this->render('A<% if 4 > 4 %>B<% else %>C<% end_if %>D'));
978
        $this->assertEquals('ACD', $this->render('A<% if 4 < 4 %>B<% else %>C<% end_if %>D'));
979
980
        // empty else_if and else tags, if this would not be supported,
981
        // the output would stop after A, thereby failing the assert
982
        $this->assertEquals('AD', $this->render('A<% if IsSet %><% else %><% end_if %>D'));
983
        $this->assertEquals(
984
            'AD',
985
            $this->render('A<% if NotSet %><% else_if IsSet %><% else %><% end_if %>D')
986
        );
987
        $this->assertEquals(
988
            'AD',
989
            $this->render('A<% if NotSet %><% else_if AlsoNotSet %><% else %><% end_if %>D')
990
        );
991
992
        // Bare words with ending space
993
        $this->assertEquals(
994
            'ABC',
995
            $this->render('A<% if "RawVal" == RawVal %>B<% end_if %>C')
996
        );
997
998
        // Else
999
        $this->assertEquals(
1000
            'ADE',
1001
            $this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% else %>D<% end_if %>E')
1002
        );
1003
1004
        // Empty if with else
1005
        $this->assertEquals(
1006
            'ABC',
1007
            $this->render('A<% if NotSet %><% else %>B<% end_if %>C')
1008
        );
1009
    }
1010
1011
    public function testBaseTagGeneration()
1012
    {
1013
        // XHTML wil have a closed base tag
1014
        $tmpl1 = '<?xml version="1.0" encoding="UTF-8"?>
1015
			<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
1016
            . ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1017
			<html>
1018
				<head><% base_tag %></head>
1019
				<body><p>test</p><body>
1020
			</html>';
1021
        $this->assertRegExp('/<head><base href=".*" \/><\/head>/', $this->render($tmpl1));
1022
1023
        // HTML4 and 5 will only have it for IE
1024
        $tmpl2 = '<!DOCTYPE html>
1025
			<html>
1026
				<head><% base_tag %></head>
1027
				<body><p>test</p><body>
1028
			</html>';
1029
        $this->assertRegExp(
1030
            '/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
1031
            $this->render($tmpl2)
1032
        );
1033
1034
1035
        $tmpl3 = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
1036
			<html>
1037
				<head><% base_tag %></head>
1038
				<body><p>test</p><body>
1039
			</html>';
1040
        $this->assertRegExp(
1041
            '/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
1042
            $this->render($tmpl3)
1043
        );
1044
1045
        // Check that the content negotiator converts to the equally legal formats
1046
        $negotiator = new ContentNegotiator();
1047
1048
        $response = new HTTPResponse($this->render($tmpl1));
1049
        $negotiator->html($response);
1050
        $this->assertRegExp(
1051
            '/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
1052
            $response->getBody()
1053
        );
1054
1055
        $response = new HTTPResponse($this->render($tmpl1));
1056
        $negotiator->xhtml($response);
1057
        $this->assertRegExp('/<head><base href=".*" \/><\/head>/', $response->getBody());
1058
    }
1059
1060
    public function testIncludeWithArguments()
1061
    {
1062
        $this->assertEquals(
1063
            $this->render('<% include SSViewerTestIncludeWithArguments %>'),
1064
            '<p>[out:Arg1]</p><p>[out:Arg2]</p>'
1065
        );
1066
1067
        $this->assertEquals(
1068
            $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A %>'),
1069
            '<p>A</p><p>[out:Arg2]</p>'
1070
        );
1071
1072
        $this->assertEquals(
1073
            $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A, Arg2=B %>'),
1074
            '<p>A</p><p>B</p>'
1075
        );
1076
1077
        $this->assertEquals(
1078
            $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>'),
1079
            '<p>A Bare String</p><p>B Bare String</p>'
1080
        );
1081
1082
        $this->assertEquals(
1083
            $this->render(
1084
                '<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=$B %>',
1085
                new ArrayData(array('B' => 'Bar'))
1086
            ),
1087
            '<p>A</p><p>Bar</p>'
1088
        );
1089
1090
        $this->assertEquals(
1091
            $this->render(
1092
                '<% include SSViewerTestIncludeWithArguments Arg1="A" %>',
1093
                new ArrayData(array('Arg1' => 'Foo', 'Arg2' => 'Bar'))
1094
            ),
1095
            '<p>A</p><p>Bar</p>'
1096
        );
1097
1098
        $this->assertEquals(
1099
            $this->render(
1100
                '<% include SSViewerTestIncludeScopeInheritanceWithArgsInLoop Title="SomeArg" %>',
1101
                new ArrayData(
1102
                    array('Items' => new ArrayList(
1103
                        array(
1104
                        new ArrayData(array('Title' => 'Foo')),
1105
                        new ArrayData(array('Title' => 'Bar'))
1106
                        )
1107
                    ))
1108
                )
1109
            ),
1110
            'SomeArg - Foo - Bar - SomeArg'
1111
        );
1112
1113
        $this->assertEquals(
1114
            $this->render(
1115
                '<% include SSViewerTestIncludeScopeInheritanceWithArgsInWith Title="A" %>',
1116
                new ArrayData(array('Item' => new ArrayData(array('Title' =>'B'))))
1117
            ),
1118
            'A - B - A'
1119
        );
1120
1121
        $this->assertEquals(
1122
            $this->render(
1123
                '<% include SSViewerTestIncludeScopeInheritanceWithArgsInNestedWith Title="A" %>',
1124
                new ArrayData(
1125
                    array(
1126
                    'Item' => new ArrayData(
1127
                        array(
1128
                        'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
1129
                        )
1130
                    ))
1131
                )
1132
            ),
1133
            'A - B - C - B - A'
1134
        );
1135
1136
        $this->assertEquals(
1137
            $this->render(
1138
                '<% include SSViewerTestIncludeScopeInheritanceWithUpAndTop Title="A" %>',
1139
                new ArrayData(
1140
                    array(
1141
                    'Item' => new ArrayData(
1142
                        array(
1143
                        'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
1144
                        )
1145
                    ))
1146
                )
1147
            ),
1148
            'A - A - A'
1149
        );
1150
1151
        $data = new ArrayData(
1152
            array(
1153
            'Nested' => new ArrayData(
1154
                array(
1155
                'Object' => new ArrayData(array('Key' => 'A'))
1156
                )
1157
            ),
1158
            'Object' => new ArrayData(array('Key' => 'B'))
1159
            )
1160
        );
1161
1162
        $tmpl = SSViewer::fromString('<% include SSViewerTestIncludeObjectArguments A=$Nested.Object, B=$Object %>');
1163
        $res  = $tmpl->process($data);
1164
        $this->assertEqualIgnoringWhitespace('A B', $res, 'Objects can be passed as named arguments');
1165
    }
1166
1167
    public function testNamespaceInclude()
1168
    {
1169
        $data = new ArrayData([]);
1170
1171
        $this->assertEquals(
1172
            "tests:( NamespaceInclude\n )",
1173
            $this->render('tests:( <% include Namespace\NamespaceInclude %> )', $data),
1174
            'Backslashes work for namespace references in includes'
1175
        );
1176
1177
        $this->assertEquals(
1178
            "tests:( NamespaceInclude\n )",
1179
            $this->render('tests:( <% include Namespace\\NamespaceInclude %> )', $data),
1180
            'Escaped backslashes work for namespace references in includes'
1181
        );
1182
1183
        $this->assertEquals(
1184
            "tests:( NamespaceInclude\n )",
1185
            $this->render('tests:( <% include Namespace/NamespaceInclude %> )', $data),
1186
            'Forward slashes work for namespace references in includes'
1187
        );
1188
    }
1189
1190
    /**
1191
     * Test search for includes fallback to non-includes folder
1192
     */
1193
    public function testIncludeFallbacks()
1194
    {
1195
        $data = new ArrayData([]);
1196
1197
        $this->assertEquals(
1198
            "tests:( Namespace/Includes/IncludedTwice.ss\n )",
1199
            $this->render('tests:( <% include Namespace\\IncludedTwice %> )', $data),
1200
            'Prefer Includes in the Includes folder'
1201
        );
1202
1203
        $this->assertEquals(
1204
            "tests:( Namespace/Includes/IncludedOnceSub.ss\n )",
1205
            $this->render('tests:( <% include Namespace\\IncludedOnceSub %> )', $data),
1206
            'Includes in only Includes folder can be found'
1207
        );
1208
1209
        $this->assertEquals(
1210
            "tests:( Namespace/IncludedOnceBase.ss\n )",
1211
            $this->render('tests:( <% include Namespace\\IncludedOnceBase %> )', $data),
1212
            'Includes outside of Includes folder can be found'
1213
        );
1214
    }
1215
1216
    public function testRecursiveInclude()
1217
    {
1218
        $view = new SSViewer(array('Includes/SSViewerTestRecursiveInclude'));
1219
1220
        $data = new ArrayData(
1221
            array(
1222
            'Title' => 'A',
1223
            'Children' => new ArrayList(
1224
                array(
1225
                new ArrayData(
1226
                    array(
1227
                    'Title' => 'A1',
1228
                    'Children' => new ArrayList(
1229
                        array(
1230
                        new ArrayData(array( 'Title' => 'A1 i', )),
1231
                        new ArrayData(array( 'Title' => 'A1 ii', )),
1232
                        )
1233
                    ),
1234
                    )
1235
                ),
1236
                new ArrayData(array( 'Title' => 'A2', )),
1237
                new ArrayData(array( 'Title' => 'A3', )),
1238
                )
1239
            ),
1240
            )
1241
        );
1242
1243
        $result = $view->process($data);
1244
        // We don't care about whitespace
1245
        $rationalisedResult = trim(preg_replace('/\s+/', ' ', $result));
1246
1247
        $this->assertEquals('A A1 A1 i A1 ii A2 A3', $rationalisedResult);
1248
    }
1249
1250
    public function assertEqualIgnoringWhitespace($a, $b, $message = '')
1251
    {
1252
        $this->assertEquals(preg_replace('/\s+/', '', $a), preg_replace('/\s+/', '', $b), $message);
1253
    }
1254
1255
    /**
1256
     * See {@link ViewableDataTest} for more extensive casting tests,
1257
     * this test just ensures that basic casting is correctly applied during template parsing.
1258
     */
1259
    public function testCastingHelpers()
1260
    {
1261
        $vd = new SSViewerTest\TestViewableData();
1262
        $vd->TextValue = '<b>html</b>';
1263
        $vd->HTMLValue = '<b>html</b>';
1264
        $vd->UncastedValue = '<b>html</b>';
1265
1266
        // Value casted as "Text"
1267
        $this->assertEquals(
1268
            '&lt;b&gt;html&lt;/b&gt;',
1269
            $t = SSViewer::fromString('$TextValue')->process($vd)
1270
        );
1271
        $this->assertEquals(
1272
            '<b>html</b>',
1273
            $t = SSViewer::fromString('$TextValue.RAW')->process($vd)
1274
        );
1275
        $this->assertEquals(
1276
            '&lt;b&gt;html&lt;/b&gt;',
1277
            $t = SSViewer::fromString('$TextValue.XML')->process($vd)
1278
        );
1279
1280
        // Value casted as "HTMLText"
1281
        $this->assertEquals(
1282
            '<b>html</b>',
1283
            $t = SSViewer::fromString('$HTMLValue')->process($vd)
1284
        );
1285
        $this->assertEquals(
1286
            '<b>html</b>',
1287
            $t = SSViewer::fromString('$HTMLValue.RAW')->process($vd)
1288
        );
1289
        $this->assertEquals(
1290
            '&lt;b&gt;html&lt;/b&gt;',
1291
            $t = SSViewer::fromString('$HTMLValue.XML')->process($vd)
1292
        );
1293
1294
        // Uncasted value (falls back to ViewableData::$default_cast="Text")
1295
        $vd = new SSViewerTest\TestViewableData();
1296
        $vd->UncastedValue = '<b>html</b>';
1297
        $this->assertEquals(
1298
            '&lt;b&gt;html&lt;/b&gt;',
1299
            $t = SSViewer::fromString('$UncastedValue')->process($vd)
1300
        );
1301
        $this->assertEquals(
1302
            '<b>html</b>',
1303
            $t = SSViewer::fromString('$UncastedValue.RAW')->process($vd)
1304
        );
1305
        $this->assertEquals(
1306
            '&lt;b&gt;html&lt;/b&gt;',
1307
            $t = SSViewer::fromString('$UncastedValue.XML')->process($vd)
1308
        );
1309
    }
1310
1311
    public function testSSViewerBasicIteratorSupport()
1312
    {
1313
        $data = new ArrayData(
1314
            array(
1315
            'Set' => new ArrayList(
1316
                array(
1317
                new SSViewerTest\TestObject("1"),
1318
                new SSViewerTest\TestObject("2"),
1319
                new SSViewerTest\TestObject("3"),
1320
                new SSViewerTest\TestObject("4"),
1321
                new SSViewerTest\TestObject("5"),
1322
                new SSViewerTest\TestObject("6"),
1323
                new SSViewerTest\TestObject("7"),
1324
                new SSViewerTest\TestObject("8"),
1325
                new SSViewerTest\TestObject("9"),
1326
                new SSViewerTest\TestObject("10"),
1327
                )
1328
            )
1329
            )
1330
        );
1331
1332
        //base test
1333
        $result = $this->render('<% loop Set %>$Number<% end_loop %>', $data);
1334
        $this->assertEquals("12345678910", $result, "Numbers rendered in order");
1335
1336
        //test First
1337
        $result = $this->render('<% loop Set %><% if First %>$Number<% end_if %><% end_loop %>', $data);
1338
        $this->assertEquals("1", $result, "Only the first number is rendered");
1339
1340
        //test Last
1341
        $result = $this->render('<% loop Set %><% if Last %>$Number<% end_if %><% end_loop %>', $data);
1342
        $this->assertEquals("10", $result, "Only the last number is rendered");
1343
1344
        //test Even
1345
        $result = $this->render('<% loop Set %><% if Even() %>$Number<% end_if %><% end_loop %>', $data);
1346
        $this->assertEquals("246810", $result, "Even numbers rendered in order");
1347
1348
        //test Even with quotes
1349
        $result = $this->render('<% loop Set %><% if Even("1") %>$Number<% end_if %><% end_loop %>', $data);
1350
        $this->assertEquals("246810", $result, "Even numbers rendered in order");
1351
1352
        //test Even without quotes
1353
        $result = $this->render('<% loop Set %><% if Even(1) %>$Number<% end_if %><% end_loop %>', $data);
1354
        $this->assertEquals("246810", $result, "Even numbers rendered in order");
1355
1356
        //test Even with zero-based start index
1357
        $result = $this->render('<% loop Set %><% if Even("0") %>$Number<% end_if %><% end_loop %>', $data);
1358
        $this->assertEquals("13579", $result, "Even (with zero-based index) numbers rendered in order");
1359
1360
        //test Odd
1361
        $result = $this->render('<% loop Set %><% if Odd %>$Number<% end_if %><% end_loop %>', $data);
1362
        $this->assertEquals("13579", $result, "Odd numbers rendered in order");
1363
1364
        //test FirstLast
1365
        $result = $this->render('<% loop Set %><% if FirstLast %>$Number$FirstLast<% end_if %><% end_loop %>', $data);
1366
        $this->assertEquals("1first10last", $result, "First and last numbers rendered in order");
1367
1368
        //test Middle
1369
        $result = $this->render('<% loop Set %><% if Middle %>$Number<% end_if %><% end_loop %>', $data);
1370
        $this->assertEquals("23456789", $result, "Middle numbers rendered in order");
1371
1372
        //test MiddleString
1373
        $result = $this->render(
1374
            '<% loop Set %><% if MiddleString == "middle" %>$Number$MiddleString<% end_if %>'
1375
            . '<% end_loop %>',
1376
            $data
1377
        );
1378
        $this->assertEquals(
1379
            "2middle3middle4middle5middle6middle7middle8middle9middle",
1380
            $result,
1381
            "Middle numbers rendered in order"
1382
        );
1383
1384
        //test EvenOdd
1385
        $result = $this->render('<% loop Set %>$EvenOdd<% end_loop %>', $data);
1386
        $this->assertEquals(
1387
            "oddevenoddevenoddevenoddevenoddeven",
1388
            $result,
1389
            "Even and Odd is returned in sequence numbers rendered in order"
1390
        );
1391
1392
        //test Pos
1393
        $result = $this->render('<% loop Set %>$Pos<% end_loop %>', $data);
1394
        $this->assertEquals("12345678910", $result, '$Pos is rendered in order');
1395
1396
        //test Pos
1397
        $result = $this->render('<% loop Set %>$Pos(0)<% end_loop %>', $data);
1398
        $this->assertEquals("0123456789", $result, '$Pos(0) is rendered in order');
1399
1400
        //test FromEnd
1401
        $result = $this->render('<% loop Set %>$FromEnd<% end_loop %>', $data);
1402
        $this->assertEquals("10987654321", $result, '$FromEnd is rendered in order');
1403
1404
        //test FromEnd
1405
        $result = $this->render('<% loop Set %>$FromEnd(0)<% end_loop %>', $data);
1406
        $this->assertEquals("9876543210", $result, '$FromEnd(0) rendered in order');
1407
1408
        //test Total
1409
        $result = $this->render('<% loop Set %>$TotalItems<% end_loop %>', $data);
1410
        $this->assertEquals("10101010101010101010", $result, "10 total items X 10 are returned");
1411
1412
        //test Modulus
1413
        $result = $this->render('<% loop Set %>$Modulus(2,1)<% end_loop %>', $data);
1414
        $this->assertEquals("1010101010", $result, "1-indexed pos modular divided by 2 rendered in order");
1415
1416
        //test MultipleOf 3
1417
        $result = $this->render('<% loop Set %><% if MultipleOf(3) %>$Number<% end_if %><% end_loop %>', $data);
1418
        $this->assertEquals("369", $result, "Only numbers that are multiples of 3 are returned");
1419
1420
        //test MultipleOf 4
1421
        $result = $this->render('<% loop Set %><% if MultipleOf(4) %>$Number<% end_if %><% end_loop %>', $data);
1422
        $this->assertEquals("48", $result, "Only numbers that are multiples of 4 are returned");
1423
1424
        //test MultipleOf 5
1425
        $result = $this->render('<% loop Set %><% if MultipleOf(5) %>$Number<% end_if %><% end_loop %>', $data);
1426
        $this->assertEquals("510", $result, "Only numbers that are multiples of 5 are returned");
1427
1428
        //test MultipleOf 10
1429
        $result = $this->render('<% loop Set %><% if MultipleOf(10,1) %>$Number<% end_if %><% end_loop %>', $data);
1430
        $this->assertEquals(
1431
            "10",
1432
            $result,
1433
            "Only numbers that are multiples of 10 (with 1-based indexing) are returned"
1434
        );
1435
1436
        //test MultipleOf 9 zero-based
1437
        $result = $this->render('<% loop Set %><% if MultipleOf(9,0) %>$Number<% end_if %><% end_loop %>', $data);
1438
        $this->assertEquals(
1439
            "110",
1440
            $result,
1441
            "Only numbers that are multiples of 9 with zero-based indexing are returned. (The first and last item)"
1442
        );
1443
1444
        //test MultipleOf 11
1445
        $result = $this->render('<% loop Set %><% if MultipleOf(11) %>$Number<% end_if %><% end_loop %>', $data);
1446
        $this->assertEquals("", $result, "Only numbers that are multiples of 11 are returned. I.e. nothing returned");
1447
    }
1448
1449
    /**
1450
     * Test $Up works when the scope $Up refers to was entered with a "with" block
1451
     */
1452
    public function testUpInWith()
1453
    {
1454
1455
        // Data to run the loop tests on - three levels deep
1456
        $data = new ArrayData([
1457
            'Name' => 'Top',
1458
            'Foo' => new ArrayData([
1459
                'Name' => 'Foo',
1460
                'Bar' => new ArrayData([
1461
                    'Name' => 'Bar',
1462
                    'Baz' => new ArrayData([
1463
                        'Name' => 'Baz'
1464
                    ]),
1465
                    'Qux' => new ArrayData([
1466
                        'Name' => 'Qux'
1467
                    ])
1468
                ])
1469
            ])
1470
        ]);
1471
1472
        // Basic functionality
1473
        $this->assertEquals(
1474
            'BarFoo',
1475
            $this->render('<% with Foo %><% with Bar %>{$Name}{$Up.Name}<% end_with %><% end_with %>', $data)
1476
        );
1477
1478
        // Two level with block, up refers to internally referenced Bar
1479
        $this->assertEquals(
1480
            'BarTop',
1481
            $this->render('<% with Foo.Bar %>{$Name}{$Up.Name}<% end_with %>', $data)
1482
        );
1483
1484
        // Stepping up & back down the scope tree
1485
        $this->assertEquals(
1486
            'BazFooBar',
1487
            $this->render('<% with Foo.Bar.Baz %>{$Name}{$Up.Foo.Name}{$Up.Foo.Bar.Name}<% end_with %>', $data)
1488
        );
1489
1490
        // Using $Up in a with block
1491
        $this->assertEquals(
1492
            'BazTopBar',
1493
            $this->render(
1494
                '<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}{$Foo.Bar.Name}<% end_with %>'
1495
                . '<% end_with %>',
1496
                $data
1497
            )
1498
        );
1499
1500
        // Stepping up & back down the scope tree with with blocks
1501
        $this->assertEquals(
1502
            'BazTopBarTopBaz',
1503
            $this->render(
1504
                '<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}<% with Foo.Bar %>{$Name}<% end_with %>'
1505
                . '{$Name}<% end_with %>{$Name}<% end_with %>',
1506
                $data
1507
            )
1508
        );
1509
1510
        // Using $Up.Up, where first $Up points to a previous scope entered using $Up, thereby skipping up to Foo
1511
        $this->assertEquals(
1512
            'Foo',
1513
            $this->render(
1514
                '<% with Foo %><% with Bar %><% with Baz %>{$Up.Up.Name}<% end_with %><% end_with %>'
1515
                . '<% end_with %>',
1516
                $data
1517
            )
1518
        );
1519
1520
        // Using $Up as part of a lookup chain in <% with %>
1521
        $this->assertEquals(
1522
            'Top',
1523
            $this->render('<% with Foo.Bar.Baz.Up.Qux %>{$Up.Name}<% end_with %>', $data)
1524
        );
1525
    }
1526
1527
    /**
1528
     * @expectedException \LogicException
1529
     * @expectedExceptionMessage Up called when we're already at the top of the scope
1530
     */
1531
    public function testTooManyUps()
1532
    {
1533
        $data = new ArrayData([
1534
            'Foo' => new ArrayData([
1535
                'Name' => 'Foo',
1536
                'Bar' => new ArrayData([
1537
                    'Name' => 'Bar'
1538
                ])
1539
            ])
1540
        ]);
1541
1542
        $this->assertEquals(
1543
            'Foo',
1544
            $this->render('<% with Foo.Bar %>{$Up.Up.Name}<% end_with %>', $data)
1545
        );
1546
    }
1547
1548
    /**
1549
     * Test $Up works when the scope $Up refers to was entered with a "loop" block
1550
     */
1551
    public function testUpInLoop()
1552
    {
1553
1554
        // Data to run the loop tests on - one sequence of three items, each with a subitem
1555
        $data = new ArrayData(
1556
            array(
1557
            'Name' => 'Top',
1558
            'Foo' => new ArrayList(
1559
                array(
1560
                new ArrayData(
1561
                    array(
1562
                    'Name' => '1',
1563
                    'Sub' => new ArrayData(
1564
                        array(
1565
                        'Name' => 'Bar'
1566
                        )
1567
                    )
1568
                    )
1569
                ),
1570
                new ArrayData(
1571
                    array(
1572
                    'Name' => '2',
1573
                    'Sub' => new ArrayData(
1574
                        array(
1575
                        'Name' => 'Baz'
1576
                        )
1577
                    )
1578
                    )
1579
                ),
1580
                new ArrayData(
1581
                    array(
1582
                    'Name' => '3',
1583
                    'Sub' => new ArrayData(
1584
                        array(
1585
                        'Name' => 'Qux'
1586
                        )
1587
                    )
1588
                    )
1589
                )
1590
                )
1591
            )
1592
            )
1593
        );
1594
1595
        // Make sure inside a loop, $Up refers to the current item of the loop
1596
        $this->assertEqualIgnoringWhitespace(
1597
            '111 222 333',
1598
            $this->render(
1599
                '<% loop $Foo %>$Name<% with $Sub %>$Up.Name<% end_with %>$Name<% end_loop %>',
1600
                $data
1601
            )
1602
        );
1603
1604
        // Make sure inside a loop, looping over $Up uses a separate iterator,
1605
        // and doesn't interfere with the original iterator
1606
        $this->assertEqualIgnoringWhitespace(
1607
            '1Bar123Bar1 2Baz123Baz2 3Qux123Qux3',
1608
            $this->render(
1609
                '<% loop $Foo %>
1610
					$Name
1611
					<% with $Sub %>
1612
						$Name
1613
						<% loop $Up %>$Name<% end_loop %>
1614
						$Name
1615
					<% end_with %>
1616
					$Name
1617
				<% end_loop %>',
1618
                $data
1619
            )
1620
        );
1621
1622
        // Make sure inside a loop, looping over $Up uses a separate iterator,
1623
        // and doesn't interfere with the original iterator or local lookups
1624
        $this->assertEqualIgnoringWhitespace(
1625
            '1 Bar1 123 1Bar 1   2 Baz2 123 2Baz 2   3 Qux3 123 3Qux 3',
1626
            $this->render(
1627
                '<% loop $Foo %>
1628
					$Name
1629
					<% with $Sub %>
1630
						{$Name}{$Up.Name}
1631
						<% loop $Up %>$Name<% end_loop %>
1632
						{$Up.Name}{$Name}
1633
					<% end_with %>
1634
					$Name
1635
				<% end_loop %>',
1636
                $data
1637
            )
1638
        );
1639
    }
1640
1641
    /**
1642
     * Test that nested loops restore the loop variables correctly when pushing and popping states
1643
     */
1644
    public function testNestedLoops()
1645
    {
1646
1647
        // Data to run the loop tests on - one sequence of three items, one with child elements
1648
        // (of a different size to the main sequence)
1649
        $data = new ArrayData(
1650
            array(
1651
            'Foo' => new ArrayList(
1652
                array(
1653
                new ArrayData(
1654
                    array(
1655
                    'Name' => '1',
1656
                    'Children' => new ArrayList(
1657
                        array(
1658
                        new ArrayData(
1659
                            array(
1660
                            'Name' => 'a'
1661
                            )
1662
                        ),
1663
                        new ArrayData(
1664
                            array(
1665
                            'Name' => 'b'
1666
                            )
1667
                        ),
1668
                        )
1669
                    ),
1670
                    )
1671
                ),
1672
                new ArrayData(
1673
                    array(
1674
                    'Name' => '2',
1675
                    'Children' => new ArrayList(),
1676
                    )
1677
                ),
1678
                new ArrayData(
1679
                    array(
1680
                    'Name' => '3',
1681
                    'Children' => new ArrayList(),
1682
                    )
1683
                ),
1684
                )
1685
            ),
1686
            )
1687
        );
1688
1689
        // Make sure that including a loop inside a loop will not destroy the internal count of
1690
        // items, checked by using "Last"
1691
        $this->assertEqualIgnoringWhitespace(
1692
            '1ab23last',
1693
            $this->render(
1694
                '<% loop $Foo %>$Name<% loop Children %>$Name<% end_loop %><% if Last %>last<% end_if %>'
1695
                . '<% end_loop %>',
1696
                $data
1697
            )
1698
        );
1699
    }
1700
1701
    public function testLayout()
1702
    {
1703
        $this->useTestTheme(
1704
            __DIR__ . '/SSViewerTest',
1705
            'layouttest',
1706
            function () {
1707
                $template = new SSViewer(array('Page'));
1708
                $this->assertEquals("Foo\n\n", $template->process(new ArrayData(array())));
1709
1710
                $template = new SSViewer(array('Shortcodes', 'Page'));
1711
                $this->assertEquals("[file_link]\n\n", $template->process(new ArrayData(array())));
1712
            }
1713
        );
1714
    }
1715
1716
    /**
1717
     * @covers \SilverStripe\View\SSViewer::get_templates_by_class()
1718
     */
1719
    public function testGetTemplatesByClass()
1720
    {
1721
        $this->useTestTheme(
1722
            __DIR__ . '/SSViewerTest',
1723
            'layouttest',
1724
            function () {
1725
            // Test passing a string
1726
                $templates = SSViewer::get_templates_by_class(
1727
                    SSViewerTestModelController::class,
1728
                    '',
1729
                    Controller::class
1730
                );
1731
                $this->assertEquals(
1732
                    [
1733
                    SSViewerTestModelController::class,
1734
                    [
1735
                        'type' => 'Includes',
1736
                        SSViewerTestModelController::class,
1737
                    ],
1738
                    SSViewerTestModel::class,
1739
                    Controller::class,
1740
                    [
1741
                        'type' => 'Includes',
1742
                        Controller::class,
1743
                    ],
1744
                    ],
1745
                    $templates
1746
                );
1747
1748
            // Test to ensure we're stopping at the base class.
1749
                $templates = SSViewer::get_templates_by_class(
1750
                    SSViewerTestModelController::class,
1751
                    '',
1752
                    SSViewerTestModelController::class
1753
                );
1754
                $this->assertEquals(
1755
                    [
1756
                    SSViewerTestModelController::class,
1757
                    [
1758
                        'type' => 'Includes',
1759
                        SSViewerTestModelController::class,
1760
                    ],
1761
                    SSViewerTestModel::class,
1762
                    ],
1763
                    $templates
1764
                );
1765
1766
            // Make sure we can search templates by suffix.
1767
                $templates = SSViewer::get_templates_by_class(
1768
                    SSViewerTestModel::class,
1769
                    'Controller',
1770
                    DataObject::class
1771
                );
1772
                $this->assertEquals(
1773
                    [
1774
                    SSViewerTestModelController::class,
1775
                    [
1776
                        'type' => 'Includes',
1777
                        SSViewerTestModelController::class,
1778
                    ],
1779
                    DataObject::class . 'Controller',
1780
                    [
1781
                        'type' => 'Includes',
1782
                        DataObject::class . 'Controller',
1783
                    ],
1784
                    ],
1785
                    $templates
1786
                );
1787
1788
                // Let's throw something random in there.
1789
                $this->expectException(InvalidArgumentException::class);
1790
                SSViewer::get_templates_by_class(null);
1791
            }
1792
        );
1793
    }
1794
1795
    public function testRewriteHashlinks()
1796
    {
1797
        SSViewer::setRewriteHashLinksDefault(true);
1798
1799
        $_SERVER['HTTP_HOST'] = 'www.mysite.com';
1800
        $_SERVER['REQUEST_URI'] = '//file.com?foo"onclick="alert(\'xss\')""';
1801
1802
        // Emulate SSViewer::process()
1803
        // Note that leading double slashes have been rewritten to prevent these being mis-interepreted
1804
        // as protocol-less absolute urls
1805
        $base = Convert::raw2att('/file.com?foo"onclick="alert(\'xss\')""');
1806
1807
        $tmplFile = TEMP_PATH . DIRECTORY_SEPARATOR . 'SSViewerTest_testRewriteHashlinks_' . sha1(rand()) . '.ss';
1808
1809
        // Note: SSViewer_FromString doesn't rewrite hash links.
1810
        file_put_contents(
1811
            $tmplFile,
1812
            '<!DOCTYPE html>
1813
			<html>
1814
				<head><% base_tag %></head>
1815
				<body>
1816
				<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>
1817
				$ExternalInsertedLink
1818
				<a class="inline" href="#anchor">InlineLink</a>
1819
				$InsertedLink
1820
				<svg><use xlink:href="#sprite"></use></svg>
1821
				<body>
1822
			</html>'
1823
        );
1824
        $tmpl = new SSViewer($tmplFile);
1825
        $obj = new ViewableData();
1826
        $obj->InsertedLink = DBField::create_field(
1827
            'HTMLFragment',
1828
            '<a class="inserted" href="#anchor">InsertedLink</a>'
1829
        );
1830
        $obj->ExternalInsertedLink = DBField::create_field(
1831
            'HTMLFragment',
1832
            '<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>'
1833
        );
1834
        $result = $tmpl->process($obj);
1835
        $this->assertContains(
1836
            '<a class="inserted" href="' . $base . '#anchor">InsertedLink</a>',
1837
            $result
1838
        );
1839
        $this->assertContains(
1840
            '<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>',
1841
            $result
1842
        );
1843
        $this->assertContains(
1844
            '<a class="inline" href="' . $base . '#anchor">InlineLink</a>',
1845
            $result
1846
        );
1847
        $this->assertContains(
1848
            '<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>',
1849
            $result
1850
        );
1851
        $this->assertContains(
1852
            '<svg><use xlink:href="#sprite"></use></svg>',
1853
            $result,
1854
            'SSTemplateParser should only rewrite anchor hrefs'
1855
        );
1856
1857
        unlink($tmplFile);
1858
    }
1859
1860
    public function testRewriteHashlinksInPhpMode()
1861
    {
1862
        SSViewer::setRewriteHashLinksDefault('php');
1863
1864
        $tmplFile = TEMP_PATH . DIRECTORY_SEPARATOR . 'SSViewerTest_testRewriteHashlinksInPhpMode_'
1865
            . sha1(rand()) . '.ss';
1866
1867
        // Note: SSViewer_FromString doesn't rewrite hash links.
1868
        file_put_contents(
1869
            $tmplFile,
1870
            '<!DOCTYPE html>
1871
			<html>
1872
				<head><% base_tag %></head>
1873
				<body>
1874
				<a class="inline" href="#anchor">InlineLink</a>
1875
				$InsertedLink
1876
				<svg><use xlink:href="#sprite"></use></svg>
1877
				<body>
1878
			</html>'
1879
        );
1880
        $tmpl = new SSViewer($tmplFile);
1881
        $obj = new ViewableData();
1882
        $obj->InsertedLink = DBField::create_field(
1883
            'HTMLFragment',
1884
            '<a class="inserted" href="#anchor">InsertedLink</a>'
1885
        );
1886
        $result = $tmpl->process($obj);
1887
1888
        $code = '<a class="inserted" href="<?php echo \SilverStripe\Core\Convert::raw2att(preg_replace("/^(\/)+/", "/",'
1889
            . ' $_SERVER[\'REQUEST_URI\'])); ?>#anchor">InsertedLink</a>';
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_VARIABLE, expecting ',' or ')' on line 1889 at column 16
Loading history...
1890
        $this->assertContains($code, $result);
1891
        // TODO Fix inline links in PHP mode
1892
        // $this->assertContains(
1893
        //  '<a class="inline" href="<?php echo str_replace(',
1894
        //  $result
1895
        // );
1896
        $this->assertContains(
1897
            '<svg><use xlink:href="#sprite"></use></svg>',
1898
            $result,
1899
            'SSTemplateParser should only rewrite anchor hrefs'
1900
        );
1901
1902
        unlink($tmplFile);
1903
    }
1904
1905
    public function testRenderWithSourceFileComments()
1906
    {
1907
        SSViewer::config()->update('source_file_comments', true);
1908
        $i = __DIR__ . '/SSViewerTest/templates/Includes';
1909
        $f = __DIR__ . '/SSViewerTest/templates/SSViewerTestComments';
1910
        $templates = array(
1911
        array(
1912
            'name' => 'SSViewerTestCommentsFullSource',
1913
            'expected' => ""
1914
                . "<!doctype html>"
1915
                . "<!-- template $f/SSViewerTestCommentsFullSource.ss -->"
1916
                . "<html>"
1917
                . "\t<head></head>"
1918
                . "\t<body></body>"
1919
                . "</html>"
1920
                . "<!-- end template $f/SSViewerTestCommentsFullSource.ss -->",
1921
        ),
1922
        array(
1923
            'name' => 'SSViewerTestCommentsFullSourceHTML4Doctype',
1924
            'expected' => ""
1925
                . "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML "
1926
                . "4.01//EN\"\t\t\"http://www.w3.org/TR/html4/strict.dtd\">"
1927
                . "<!-- template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->"
1928
                . "<html>"
1929
                . "\t<head></head>"
1930
                . "\t<body></body>"
1931
                . "</html>"
1932
                . "<!-- end template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->",
1933
        ),
1934
        array(
1935
            'name' => 'SSViewerTestCommentsFullSourceNoDoctype',
1936
            'expected' => ""
1937
                . "<html><!-- template $f/SSViewerTestCommentsFullSourceNoDoctype.ss -->"
1938
                . "\t<head></head>"
1939
                . "\t<body></body>"
1940
                . "<!-- end template $f/SSViewerTestCommentsFullSourceNoDoctype.ss --></html>",
1941
        ),
1942
        array(
1943
            'name' => 'SSViewerTestCommentsFullSourceIfIE',
1944
            'expected' => ""
1945
                . "<!doctype html>"
1946
                . "<!-- template $f/SSViewerTestCommentsFullSourceIfIE.ss -->"
1947
                . "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1948
                . "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1949
                . "<!--[if !IE]><!--> <html class='no-ie'> <!--<![endif]-->"
1950
                . "\t<head></head>"
1951
                . "\t<body></body>"
1952
                . "</html>"
1953
                . "<!-- end template $f/SSViewerTestCommentsFullSourceIfIE.ss -->",
1954
        ),
1955
        array(
1956
            'name' => 'SSViewerTestCommentsFullSourceIfIENoDoctype',
1957
            'expected' => ""
1958
                . "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1959
                . "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1960
                . "<!--[if !IE]><!--> <html class='no-ie'>"
1961
                . "<!-- template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss -->"
1962
                . " <!--<![endif]-->"
1963
                . "\t<head></head>"
1964
                . "\t<body></body>"
1965
                . "<!-- end template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss --></html>",
1966
        ),
1967
        array(
1968
            'name' => 'SSViewerTestCommentsPartialSource',
1969
            'expected' =>
1970
            "<!-- template $f/SSViewerTestCommentsPartialSource.ss -->"
1971
                . "<div class='typography'></div>"
1972
                . "<!-- end template $f/SSViewerTestCommentsPartialSource.ss -->",
1973
        ),
1974
        array(
1975
            'name' => 'SSViewerTestCommentsWithInclude',
1976
            'expected' =>
1977
            "<!-- template $f/SSViewerTestCommentsWithInclude.ss -->"
1978
                . "<div class='typography'>"
1979
                . "<!-- include 'SSViewerTestCommentsInclude' -->"
1980
                . "<!-- template $i/SSViewerTestCommentsInclude.ss -->"
1981
                . "Included"
1982
                . "<!-- end template $i/SSViewerTestCommentsInclude.ss -->"
1983
                . "<!-- end include 'SSViewerTestCommentsInclude' -->"
1984
                . "</div>"
1985
                . "<!-- end template $f/SSViewerTestCommentsWithInclude.ss -->",
1986
        ),
1987
        );
1988
        foreach ($templates as $template) {
1989
            $this->_renderWithSourceFileComments('SSViewerTestComments/' . $template['name'], $template['expected']);
1990
        }
1991
    }
1992
    private function _renderWithSourceFileComments($name, $expected)
1993
    {
1994
        $viewer = new SSViewer(array($name));
1995
        $data = new ArrayData(array());
1996
        $result = $viewer->process($data);
1997
        $expected = str_replace(array("\r", "\n"), '', $expected);
1998
        $result = str_replace(array("\r", "\n"), '', $result);
1999
        $this->assertEquals($result, $expected);
2000
    }
2001
2002
    public function testLoopIteratorIterator()
2003
    {
2004
        $list = new PaginatedList(new ArrayList());
2005
        $viewer = new SSViewer_FromString('<% loop List %>$ID - $FirstName<br /><% end_loop %>');
2006
        $result = $viewer->process(new ArrayData(array('List' => $list)));
2007
        $this->assertEquals($result, '');
2008
    }
2009
2010
    public function testProcessOnlyIncludesRequirementsOnce()
2011
    {
2012
        $template = new SSViewer(array('SSViewerTestProcess'));
2013
        $basePath = $this->getCurrentRelativePath() . '/SSViewerTest';
2014
2015
        $backend = Injector::inst()->create(Requirements_Backend::class);
2016
        $backend->setCombinedFilesEnabled(false);
2017
        $backend->combineFiles(
2018
            'RequirementsTest_ab.css',
2019
            array(
2020
            $basePath . '/css/RequirementsTest_a.css',
2021
            $basePath . '/css/RequirementsTest_b.css'
2022
            )
2023
        );
2024
2025
        Requirements::set_backend($backend);
2026
2027
        $this->assertEquals(1, substr_count($template->process(new ViewableData()), "a.css"));
2028
        $this->assertEquals(1, substr_count($template->process(new ViewableData()), "b.css"));
2029
2030
        // if we disable the requirements then we should get nothing
2031
        $template->includeRequirements(false);
2032
        $this->assertEquals(0, substr_count($template->process(new ViewableData()), "a.css"));
2033
        $this->assertEquals(0, substr_count($template->process(new ViewableData()), "b.css"));
2034
    }
2035
2036
    public function testRequireCallInTemplateInclude()
2037
    {
2038
        //TODO undo skip test on the event that templates ever obtain the ability to reference
2039
        //MODULE_DIR (or something to that effect)
2040
        if (FRAMEWORK_DIR === 'framework') {
2041
            $template = new SSViewer(array('SSViewerTestProcess'));
2042
2043
            Requirements::set_suffix_requirements(false);
2044
2045
            $this->assertEquals(
2046
                1,
2047
                substr_count(
2048
                    $template->process(new ViewableData()),
2049
                    "tests/php/View/SSViewerTest/javascript/RequirementsTest_a.js"
2050
                )
2051
            );
2052
        } else {
2053
            $this->markTestSkipped(
2054
                'Requirement will always fail if the framework dir is not ' .
2055
                'named \'framework\', since templates require hard coded paths'
2056
            );
2057
        }
2058
    }
2059
2060
    public function testCallsWithArguments()
2061
    {
2062
        $data = new ArrayData(
2063
            array(
2064
            'Set' => new ArrayList(
2065
                array(
2066
                new SSViewerTest\TestObject("1"),
2067
                new SSViewerTest\TestObject("2"),
2068
                new SSViewerTest\TestObject("3"),
2069
                new SSViewerTest\TestObject("4"),
2070
                new SSViewerTest\TestObject("5"),
2071
                )
2072
            ),
2073
            'Level' => new SSViewerTest\LevelTestData(1),
2074
            'Nest' => array(
2075
            'Level' => new SSViewerTest\LevelTestData(2),
2076
            ),
2077
            )
2078
        );
2079
2080
        $tests = array(
2081
        '$Level.output(1)' => '1-1',
2082
        '$Nest.Level.output($Set.First.Number)' => '2-1',
2083
        '<% with $Set %>$Up.Level.output($First.Number)<% end_with %>' => '1-1',
2084
        '<% with $Set %>$Top.Nest.Level.output($First.Number)<% end_with %>' => '2-1',
2085
        '<% loop $Set %>$Up.Nest.Level.output($Number)<% end_loop %>' => '2-12-22-32-42-5',
2086
        '<% loop $Set %>$Top.Level.output($Number)<% end_loop %>' => '1-11-21-31-41-5',
2087
        '<% with $Nest %>$Level.output($Top.Set.First.Number)<% end_with %>' => '2-1',
2088
        '<% with $Level %>$output($Up.Set.Last.Number)<% end_with %>' => '1-5',
2089
        '<% with $Level.forWith($Set.Last.Number) %>$output("hi")<% end_with %>' => '5-hi',
2090
        '<% loop $Level.forLoop($Set.First.Number) %>$Number<% end_loop %>' => '!0',
2091
        '<% with $Nest %>
2092
				<% with $Level.forWith($Up.Set.First.Number) %>$output("hi")<% end_with %>
2093
			<% end_with %>' => '1-hi',
2094
        '<% with $Nest %>
2095
				<% loop $Level.forLoop($Top.Set.Last.Number) %>$Number<% end_loop %>
2096
			<% end_with %>' => '!0!1!2!3!4',
2097
        );
2098
2099
        foreach ($tests as $template => $expected) {
2100
            $this->assertEquals($expected, trim($this->render($template, $data)));
2101
        }
2102
    }
2103
2104
    public function testRepeatedCallsAreCached()
2105
    {
2106
        $data = new SSViewerTest\CacheTestData();
2107
        $template = '
2108
			<% if $TestWithCall %>
2109
				<% with $TestWithCall %>
2110
					{$Message}
2111
				<% end_with %>
2112
2113
				{$TestWithCall.Message}
2114
			<% end_if %>';
2115
2116
        $this->assertEquals('HiHi', preg_replace('/\s+/', '', $this->render($template, $data)));
2117
        $this->assertEquals(
2118
            1,
2119
            $data->testWithCalls,
2120
            'SSViewerTest_CacheTestData::TestWithCall() should only be called once. Subsequent calls should be cached'
2121
        );
2122
2123
        $data = new SSViewerTest\CacheTestData();
2124
        $template = '
2125
			<% if $TestLoopCall %>
2126
				<% loop $TestLoopCall %>
2127
					{$Message}
2128
				<% end_loop %>
2129
			<% end_if %>';
2130
2131
        $this->assertEquals('OneTwo', preg_replace('/\s+/', '', $this->render($template, $data)));
2132
        $this->assertEquals(
2133
            1,
2134
            $data->testLoopCalls,
2135
            'SSViewerTest_CacheTestData::TestLoopCall() should only be called once. Subsequent calls should be cached'
2136
        );
2137
    }
2138
2139
    public function testClosedBlockExtension()
2140
    {
2141
        $count = 0;
2142
        $parser = new SSTemplateParser();
2143
        $parser->addClosedBlock(
2144
            'test',
2145
            function ($res) use (&$count) {
2146
                $count++;
2147
            }
2148
        );
2149
2150
        $template = new SSViewer_FromString("<% test %><% end_test %>", $parser);
2151
        $template->process(new SSViewerTest\TestFixture());
2152
2153
        $this->assertEquals(1, $count);
2154
    }
2155
2156
    public function testOpenBlockExtension()
2157
    {
2158
        $count = 0;
2159
        $parser = new SSTemplateParser();
2160
        $parser->addOpenBlock(
2161
            'test',
2162
            function ($res) use (&$count) {
2163
                $count++;
2164
            }
2165
        );
2166
2167
        $template = new SSViewer_FromString("<% test %>", $parser);
2168
        $template->process(new SSViewerTest\TestFixture());
2169
2170
        $this->assertEquals(1, $count);
2171
    }
2172
2173
    /**
2174
     * Tests if caching for SSViewer_FromString is working
2175
     */
2176
    public function testFromStringCaching()
2177
    {
2178
        $content = 'Test content';
2179
        $cacheFile = TEMP_PATH . DIRECTORY_SEPARATOR . '.cache.' . sha1($content);
2180
        if (file_exists($cacheFile)) {
2181
            unlink($cacheFile);
2182
        }
2183
2184
        // Test global behaviors
2185
        $this->render($content, null, null);
2186
        $this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
2187
2188
        SSViewer_FromString::config()->update('cache_template', true);
2189
        $this->render($content, null, null);
2190
        $this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
2191
        unlink($cacheFile);
2192
2193
        // Test instance behaviors
2194
        $this->render($content, null, false);
2195
        $this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
2196
2197
        $this->render($content, null, true);
2198
        $this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
2199
        unlink($cacheFile);
2200
    }
2201
}
2202