Test Setup Failed
Push — master ( 210134...c17796 )
by Damian
03:18
created

tests/php/View/SSViewerTest.php (2 issues)

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\Tests\Storage\AssetStoreTest\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\ViewableData;
34
35
/**
36
 * @skipUpgrade
37
 */
38
class SSViewerTest extends SapphireTest
39
{
40
41
    /**
42
     * Backup of $_SERVER global
43
     *
44
     * @var array
45
     */
46
    protected $oldServer = array();
47
48
    protected static $extra_dataobjects = array(
49
        SSViewerTest\TestObject::class,
50
    );
51
52
    protected function setUp()
53
    {
54
        parent::setUp();
55
        SSViewer::config()->update('source_file_comments', false);
56
        SSViewer_FromString::config()->update('cache_template', false);
57
        TestAssetStore::activate('SSViewerTest');
58
        $this->oldServer = $_SERVER;
59
    }
60
61
    protected function tearDown()
62
    {
63
        $_SERVER = $this->oldServer;
64
        TestAssetStore::reset();
65
        parent::tearDown();
66
    }
67
68
    /**
69
     * Tests for {@link Config::inst()->get('SSViewer', 'theme')} for different behaviour
70
     * of user defined themes via {@link SiteConfig} and default theme
71
     * when no user themes are defined.
72
     */
73
    public function testCurrentTheme()
74
    {
75
        SSViewer::config()->update('theme', 'mytheme');
76
        $this->assertEquals(
77
            'mytheme',
78
            SSViewer::config()->uninherited('theme'),
79
            'Current theme is the default - user has not defined one'
80
        );
81
    }
82
83
    /**
84
     * Tests for themes helper functions, ensuring they behave as defined in the RFC at
85
     * https://github.com/silverstripe/silverstripe-framework/issues/5604
86
     */
87
    public function testThemesHelpers()
88
    {
89
        // Test set_themes()
90
        SSViewer::set_themes(['mytheme', '$default']);
91
        $this->assertEquals(['mytheme', '$default'], SSViewer::get_themes());
92
93
        // Ensure add_themes() prepends
94
        SSViewer::add_themes(['my_more_important_theme']);
95
        $this->assertEquals(['my_more_important_theme', 'mytheme', '$default'], SSViewer::get_themes());
96
97
        // Ensure add_themes() on theme already in cascade promotes it to the top
98
        SSViewer::add_themes(['mytheme']);
99
        $this->assertEquals(['mytheme', 'my_more_important_theme', '$default'], SSViewer::get_themes());
100
    }
101
102
    /**
103
     * Test that a template without a <head> tag still renders.
104
     */
105
    public function testTemplateWithoutHeadRenders()
106
    {
107
        $data = new ArrayData([ 'Var' => 'var value' ]);
108
        $result = $data->renderWith("SSViewerTestPartialTemplate");
109
        $this->assertEquals('Test partial template: var value', trim(preg_replace("/<!--.*-->/U", '', $result)));
110
    }
111
112
    public function testIncludeScopeInheritance()
113
    {
114
        $data = $this->getScopeInheritanceTestData();
115
        $expected = array(
116
        'Item 1 - First-ODD top:Item 1',
117
        'Item 2 - EVEN top:Item 2',
118
        'Item 3 - ODD top:Item 3',
119
        'Item 4 - EVEN top:Item 4',
120
        'Item 5 - ODD top:Item 5',
121
        'Item 6 - Last-EVEN top:Item 6',
122
        );
123
124
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritance');
125
        $this->assertExpectedStrings($result, $expected);
126
127
        // reset results for the tests that include arguments (the title is passed as an arg)
128
        $expected = array(
129
        'Item 1 _ Item 1 - First-ODD top:Item 1',
130
        'Item 2 _ Item 2 - EVEN top:Item 2',
131
        'Item 3 _ Item 3 - ODD top:Item 3',
132
        'Item 4 _ Item 4 - EVEN top:Item 4',
133
        'Item 5 _ Item 5 - ODD top:Item 5',
134
        'Item 6 _ Item 6 - Last-EVEN top:Item 6',
135
        );
136
137
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
138
        $this->assertExpectedStrings($result, $expected);
139
    }
140
141
    public function testIncludeTruthyness()
142
    {
143
        $data = new ArrayData([
144
            'Title' => 'TruthyTest',
145
            'Items' => new ArrayList([
146
                new ArrayData(['Title' => 'Item 1']),
147
                new ArrayData(['Title' => '']),
148
                new ArrayData(['Title' => true]),
149
                new ArrayData(['Title' => false]),
150
                new ArrayData(['Title' => null]),
151
                new ArrayData(['Title' => 0]),
152
                new ArrayData(['Title' => 7])
153
            ])
154
        ]);
155
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
156
157
        // We should not end up with empty values appearing as empty
158
        $expected = [
159
            'Item 1 _ Item 1 - First-ODD top:Item 1',
160
            'Untitled - EVEN top:',
161
            '1 _ 1 - ODD top:1',
162
            'Untitled - EVEN top:',
163
            'Untitled - ODD top:',
164
            'Untitled - EVEN top:0',
165
            '7 _ 7 - Last-ODD top:7',
166
        ];
167
        $this->assertExpectedStrings($result, $expected);
168
    }
169
170
    private function getScopeInheritanceTestData()
171
    {
172
        return new ArrayData([
173
            'Title' => 'TopTitleValue',
174
            'Items' => new ArrayList([
175
                new ArrayData(['Title' => 'Item 1']),
176
                new ArrayData(['Title' => 'Item 2']),
177
                new ArrayData(['Title' => 'Item 3']),
178
                new ArrayData(['Title' => 'Item 4']),
179
                new ArrayData(['Title' => 'Item 5']),
180
                new ArrayData(['Title' => 'Item 6'])
181
            ])
182
        ]);
183
    }
184
185
    private function assertExpectedStrings($result, $expected)
186
    {
187
        foreach ($expected as $expectedStr) {
188
            $this->assertTrue(
189
                (boolean) preg_match("/{$expectedStr}/", $result),
190
                "Didn't find '{$expectedStr}' in:\n{$result}"
191
            );
192
        }
193
    }
194
195
    /**
196
     * Small helper to render templates from strings
197
     *
198
     * @param  string $templateString
199
     * @param  null   $data
200
     * @param  bool   $cacheTemplate
201
     * @return string
202
     */
203
    public function render($templateString, $data = null, $cacheTemplate = false)
204
    {
205
        $t = SSViewer::fromString($templateString, $cacheTemplate);
206
        if (!$data) {
207
            $data = new SSViewerTest\TestFixture();
208
        }
209
        return trim(''.$t->process($data));
210
    }
211
212
    public function testRequirements()
213
    {
214
        /** @var Requirements_Backend|PHPUnit_Framework_MockObject_MockObject $requirements */
215
        $requirements = $this
216
            ->getMockBuilder(Requirements_Backend::class)
217
            ->setMethods(array("javascript", "css"))
218
            ->getMock();
219
        $jsFile = FRAMEWORK_DIR . '/tests/forms/a.js';
220
        $cssFile = FRAMEWORK_DIR . '/tests/forms/a.js';
221
222
        $requirements->expects($this->once())->method('javascript')->with($jsFile);
223
        $requirements->expects($this->once())->method('css')->with($cssFile);
224
225
        $origReq = Requirements::backend();
226
        Requirements::set_backend($requirements);
227
        $template = $this->render(
228
            "<% require javascript($jsFile) %>
229
		<% require css($cssFile) %>"
230
        );
231
        Requirements::set_backend($origReq);
232
233
        $this->assertFalse((bool)trim($template), "Should be no content in this return.");
234
    }
235
236
    public function testRequirementsCombine()
237
    {
238
        /** @var Requirements_Backend $testBackend */
239
        $testBackend = Injector::inst()->create(Requirements_Backend::class);
240
        $testBackend->setSuffixRequirements(false);
241
        $testBackend->setCombinedFilesEnabled(true);
242
243
        //$combinedTestFilePath = BASE_PATH . '/' . $testBackend->getCombinedFilesFolder() . '/testRequirementsCombine.js';
244
245
        $jsFile = $this->getCurrentRelativePath() . '/SSViewerTest/javascript/bad.js';
246
        $jsFileContents = file_get_contents(BASE_PATH . '/' . $jsFile);
247
        $testBackend->combineFiles('testRequirementsCombine.js', array($jsFile));
248
249
        // secondly, make sure that requirements is generated, even though minification failed
250
        $testBackend->processCombinedFiles();
251
        $js = array_keys($testBackend->getJavascript());
252
        $combinedTestFilePath = BASE_PATH . reset($js);
253
        $this->assertContains('_combinedfiles/testRequirementsCombine-4c0e97a.js', $combinedTestFilePath);
254
255
        // and make sure the combined content matches the input content, i.e. no loss of functionality
256
        if (!file_exists($combinedTestFilePath)) {
257
            $this->fail('No combined file was created at expected path: '.$combinedTestFilePath);
258
        }
259
        $combinedTestFileContents = file_get_contents($combinedTestFilePath);
260
        $this->assertContains($jsFileContents, $combinedTestFileContents);
261
    }
262
263
    public function testRequirementsMinification()
264
    {
265
        /** @var Requirements_Backend $testBackend */
266
        $testBackend = Injector::inst()->create(Requirements_Backend::class);
267
        $testBackend->setSuffixRequirements(false);
268
        $testBackend->setMinifyCombinedFiles(true);
269
        $testBackend->setCombinedFilesEnabled(true);
270
271
        $testFile = $this->getCurrentRelativePath() . '/SSViewerTest/javascript/RequirementsTest_a.js';
272
        $testFileContent = file_get_contents($testFile);
273
274
        $mockMinifier = $this->getMockBuilder(Requirements_Minifier::class)
275
        ->setMethods(['minify'])
276
        ->getMock();
277
278
        $mockMinifier->expects($this->once())
279
        ->method('minify')
280
        ->with(
281
            $testFileContent,
282
            'js',
283
            $testFile
284
        );
285
        $testBackend->setMinifier($mockMinifier);
286
        $testBackend->combineFiles('testRequirementsMinified.js', array($testFile));
287
        $testBackend->processCombinedFiles();
288
289
        $testBackend->setMinifyCombinedFiles(false);
290
        $mockMinifier->expects($this->never())
291
        ->method('minify');
292
        $testBackend->processCombinedFiles();
293
294
        $this->expectException(Exception::class);
295
        $this->expectExceptionMessageRegExp('/^Cannot minify files without a minification service defined./');
296
297
        $testBackend->setMinifyCombinedFiles(true);
298
        $testBackend->setMinifier(null);
299
        $testBackend->processCombinedFiles();
300
    }
301
302
303
304
    public function testComments()
305
    {
306
        $input = <<<SS
307
This is my template<%-- this is a comment --%>This is some content<%-- this is another comment --%>Final content
308
<%-- Alone multi
309
	line comment --%>
310
Some more content
311
Mixing content and <%-- multi
312
	line comment --%> Final final
313
content
314
SS;
315
        $output = $this->render($input);
316
        $shouldbe = <<<SS
317
This is my templateThis is some contentFinal content
318
319
Some more content
320
Mixing content and  Final final
321
content
322
SS;
323
        $this->assertEquals($shouldbe, $output);
324
    }
325
326
    public function testBasicText()
327
    {
328
        $this->assertEquals('"', $this->render('"'), 'Double-quotes are left alone');
329
        $this->assertEquals("'", $this->render("'"), 'Single-quotes are left alone');
330
        $this->assertEquals('A', $this->render('\\A'), 'Escaped characters are unescaped');
331
        $this->assertEquals('\\A', $this->render('\\\\A'), 'Escaped back-slashed are correctly unescaped');
332
    }
333
334
    public function testBasicInjection()
335
    {
336
        $this->assertEquals('[out:Test]', $this->render('$Test'), 'Basic stand-alone injection');
337
        $this->assertEquals('[out:Test]', $this->render('{$Test}'), 'Basic stand-alone wrapped injection');
338
        $this->assertEquals('A[out:Test]!', $this->render('A$Test!'), 'Basic surrounded injection');
339
        $this->assertEquals('A[out:Test]B', $this->render('A{$Test}B'), 'Basic surrounded wrapped injection');
340
341
        $this->assertEquals('A$B', $this->render('A\\$B'), 'No injection as $ escaped');
342
        $this->assertEquals('A$ B', $this->render('A$ B'), 'No injection as $ not followed by word character');
343
        $this->assertEquals('A{$ B', $this->render('A{$ B'), 'No injection as {$ not followed by word character');
344
345
        $this->assertEquals('{$Test}', $this->render('{\\$Test}'), 'Escapes can be used to avoid injection');
346
        $this->assertEquals(
347
            '{\\[out:Test]}',
348
            $this->render('{\\\\$Test}'),
349
            'Escapes before injections are correctly unescaped'
350
        );
351
    }
352
353
354
    public function testGlobalVariableCalls()
355
    {
356
        $this->assertEquals('automatic', $this->render('$SSViewerTest_GlobalAutomatic'));
357
        $this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedByString'));
358
        $this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedInArray'));
359
    }
360
361
    public function testGlobalVariableCallsWithArguments()
362
    {
363
        $this->assertEquals('zz', $this->render('$SSViewerTest_GlobalThatTakesArguments'));
364
        $this->assertEquals('zFooz', $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo")'));
365
        $this->assertEquals(
366
            'zFoo:Bar:Bazz',
367
            $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo", "Bar", "Baz")')
368
        );
369
        $this->assertEquals(
370
            'zreferencez',
371
            $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalReferencedByString)')
372
        );
373
    }
374
375
    public function testGlobalVariablesAreEscaped()
376
    {
377
        $this->assertEquals('<div></div>', $this->render('$SSViewerTest_GlobalHTMLFragment'));
378
        $this->assertEquals('&lt;div&gt;&lt;/div&gt;', $this->render('$SSViewerTest_GlobalHTMLEscaped'));
379
380
        $this->assertEquals(
381
            'z<div></div>z',
382
            $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLFragment)')
383
        );
384
        $this->assertEquals(
385
            'z&lt;div&gt;&lt;/div&gt;z',
386
            $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLEscaped)')
387
        );
388
    }
389
390
    public function testCoreGlobalVariableCalls()
391
    {
392
        $this->assertEquals(
393
            Director::absoluteBaseURL(),
394
            $this->render('{$absoluteBaseURL}'),
395
            'Director::absoluteBaseURL can be called from within template'
396
        );
397
        $this->assertEquals(
398
            Director::absoluteBaseURL(),
399
            $this->render('{$AbsoluteBaseURL}'),
400
            'Upper-case %AbsoluteBaseURL can be called from within template'
401
        );
402
403
        $this->assertEquals(
404
            Director::is_ajax(),
405
            $this->render('{$isAjax}'),
406
            'All variations of is_ajax result in the correct call'
407
        );
408
        $this->assertEquals(
409
            Director::is_ajax(),
410
            $this->render('{$IsAjax}'),
411
            'All variations of is_ajax result in the correct call'
412
        );
413
        $this->assertEquals(
414
            Director::is_ajax(),
415
            $this->render('{$is_ajax}'),
416
            'All variations of is_ajax result in the correct call'
417
        );
418
        $this->assertEquals(
419
            Director::is_ajax(),
420
            $this->render('{$Is_ajax}'),
421
            'All variations of is_ajax result in the correct call'
422
        );
423
424
        $this->assertEquals(
425
            i18n::get_locale(),
426
            $this->render('{$i18nLocale}'),
427
            'i18n template functions result correct result'
428
        );
429
        $this->assertEquals(
430
            i18n::get_locale(),
431
            $this->render('{$get_locale}'),
432
            'i18n template functions result correct result'
433
        );
434
435
        $this->assertEquals(
436
            (string)Security::getCurrentUser(),
437
            $this->render('{$CurrentMember}'),
438
            'Member template functions result correct result'
439
        );
440
        $this->assertEquals(
441
            (string)Security::getCurrentUser(),
442
            $this->render('{$CurrentUser}'),
443
            'Member template functions result correct result'
444
        );
445
        $this->assertEquals(
446
            (string)Security::getCurrentUser(),
447
            $this->render('{$currentMember}'),
448
            'Member template functions result correct result'
449
        );
450
        $this->assertEquals(
451
            (string)Security::getCurrentUser(),
452
            $this->render('{$currentUser}'),
453
            'Member template functions result correct result'
454
        );
455
456
        $this->assertEquals(
457
            SecurityToken::getSecurityID(),
458
            $this->render('{$getSecurityID}'),
459
            'SecurityToken template functions result correct result'
460
        );
461
        $this->assertEquals(
462
            SecurityToken::getSecurityID(),
463
            $this->render('{$SecurityID}'),
464
            'SecurityToken template functions result correct result'
465
        );
466
467
        $this->assertEquals(
468
            Permission::check("ADMIN"),
469
            (bool)$this->render('{$HasPerm(\'ADMIN\')}'),
470
            'Permissions template functions result correct result'
471
        );
472
        $this->assertEquals(
473
            Permission::check("ADMIN"),
474
            (bool)$this->render('{$hasPerm(\'ADMIN\')}'),
475
            'Permissions template functions result correct result'
476
        );
477
    }
478
479
    public function testNonFieldCastingHelpersNotUsedInHasValue()
480
    {
481
        // check if Link without $ in front of variable
482
        $result = $this->render(
483
            'A<% if Link %>$Link<% end_if %>B',
484
            new SSViewerTest\TestObject()
485
        );
486
        $this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if Link %>');
487
488
        // check if Link with $ in front of variable
489
        $result = $this->render(
490
            'A<% if $Link %>$Link<% end_if %>B',
491
            new SSViewerTest\TestObject()
492
        );
493
        $this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if $Link %>');
494
    }
495
496
    public function testLocalFunctionsTakePriorityOverGlobals()
497
    {
498
        $data = new ArrayData([
499
            'Page' => new SSViewerTest\TestObject()
500
        ]);
501
502
        //call method with lots of arguments
503
        $result = $this->render(
504
            '<% with Page %>$lotsOfArguments11("a","b","c","d","e","f","g","h","i","j","k")<% end_with %>',
505
            $data
506
        );
507
        $this->assertEquals("abcdefghijk", $result, "public function can accept up to 11 arguments");
508
509
        //call method that does not exist
510
        $result = $this->render('<% with Page %><% if IDoNotExist %>hello<% end_if %><% end_with %>', $data);
511
        $this->assertEquals("", $result, "Method does not exist - empty result");
512
513
        //call if that does not exist
514
        $result = $this->render('<% with Page %>$IDoNotExist("hello")<% end_with %>', $data);
515
        $this->assertEquals("", $result, "Method does not exist - empty result");
516
517
        //call method with same name as a global method (local call should take priority)
518
        $result = $this->render('<% with Page %>$absoluteBaseURL<% end_with %>', $data);
519
        $this->assertEquals(
520
            "testLocalFunctionPriorityCalled",
521
            $result,
522
            "Local Object's public function called. Did not return the actual baseURL of the current site"
523
        );
524
    }
525
526
    public function testCurrentScopeLoopWith()
527
    {
528
        // Data to run the loop tests on - one sequence of three items, each with a subitem
529
        $data = new ArrayData([
530
            'Foo' => new ArrayList([
531
                'Subocean' => new ArrayData([
532
                    'Name' => 'Higher'
533
                ]),
534
                new ArrayData([
535
                    'Sub' => new ArrayData([
536
                        'Name' => 'SubKid1'
537
                    ])
538
                ]),
539
                new ArrayData([
540
                    'Sub' => new ArrayData([
541
                        'Name' => 'SubKid2'
542
                    ])
543
                ]),
544
                new SSViewerTest\TestObject('Number6')
545
            ])
546
        ]);
547
548
        $result = $this->render(
549
            '<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',
550
            $data
551
        );
552
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop works");
553
554
        $result = $this->render(
555
            '<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',
556
            $data
557
        );
558
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop works");
559
560
        $result = $this->render('<% with Foo %>$Count<% end_with %>', $data);
561
        $this->assertEquals("4", $result, "4 items in the DataObjectSet");
562
563
        $result = $this->render(
564
            '<% with Foo %><% loop Up.Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
565
            . '<% end_if %><% end_loop %><% end_with %>',
566
            $data
567
        );
568
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop in with Up.Foo scope works");
569
570
        $result = $this->render(
571
            '<% with Foo %><% loop %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
572
            . '<% end_if %><% end_loop %><% end_with %>',
573
            $data
574
        );
575
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop in current scope works");
576
    }
577
578
    public function testObjectDotArguments()
579
    {
580
        $this->assertEquals(
581
            '[out:TestObject.methodWithOneArgument(one)]
582
				[out:TestObject.methodWithTwoArguments(one,two)]
583
				[out:TestMethod(Arg1,Arg2).Bar.Val]
584
				[out:TestMethod(Arg1,Arg2).Bar]
585
				[out:TestMethod(Arg1,Arg2)]
586
				[out:TestMethod(Arg1).Bar.Val]
587
				[out:TestMethod(Arg1).Bar]
588
				[out:TestMethod(Arg1)]',
589
            $this->render(
590
                '$TestObject.methodWithOneArgument(one)
591
				$TestObject.methodWithTwoArguments(one,two)
592
				$TestMethod(Arg1, Arg2).Bar.Val
593
				$TestMethod(Arg1, Arg2).Bar
594
				$TestMethod(Arg1, Arg2)
595
				$TestMethod(Arg1).Bar.Val
596
				$TestMethod(Arg1).Bar
597
				$TestMethod(Arg1)'
598
            )
599
        );
600
    }
601
602
    public function testEscapedArguments()
603
    {
604
        $this->assertEquals(
605
            '[out:Foo(Arg1,Arg2).Bar.Val].Suffix
606
				[out:Foo(Arg1,Arg2).Val]_Suffix
607
				[out:Foo(Arg1,Arg2)]/Suffix
608
				[out:Foo(Arg1).Bar.Val]textSuffix
609
				[out:Foo(Arg1).Bar].Suffix
610
				[out:Foo(Arg1)].Suffix
611
				[out:Foo.Bar.Val].Suffix
612
				[out:Foo.Bar].Suffix
613
				[out:Foo].Suffix',
614
            $this->render(
615
                '{$Foo(Arg1, Arg2).Bar.Val}.Suffix
616
				{$Foo(Arg1, Arg2).Val}_Suffix
617
				{$Foo(Arg1, Arg2)}/Suffix
618
				{$Foo(Arg1).Bar.Val}textSuffix
619
				{$Foo(Arg1).Bar}.Suffix
620
				{$Foo(Arg1)}.Suffix
621
				{$Foo.Bar.Val}.Suffix
622
				{$Foo.Bar}.Suffix
623
				{$Foo}.Suffix'
624
            )
625
        );
626
    }
627
628
    public function testLoopWhitespace()
629
    {
630
        $this->assertEquals(
631
            'before[out:SingleItem.Test]after
632
				beforeTestafter',
633
            $this->render(
634
                'before<% loop SingleItem %>$Test<% end_loop %>after
635
				before<% loop SingleItem %>Test<% end_loop %>after'
636
            )
637
        );
638
639
        // The control tags are removed from the output, but no whitespace
640
        // This is a quirk that could be changed, but included in the test to make the current
641
        // behaviour explicit
642
        $this->assertEquals(
643
            'before
644
645
[out:SingleItem.ItemOnItsOwnLine]
646
647
after',
648
            $this->render(
649
                'before
650
<% loop SingleItem %>
651
$ItemOnItsOwnLine
652
<% end_loop %>
653
after'
654
            )
655
        );
656
657
        // The whitespace within the control tags is preserve in a loop
658
        // This is a quirk that could be changed, but included in the test to make the current
659
        // behaviour explicit
660
        $this->assertEquals(
661
            'before
662
663
[out:Loop3.ItemOnItsOwnLine]
664
665
[out:Loop3.ItemOnItsOwnLine]
666
667
[out:Loop3.ItemOnItsOwnLine]
668
669
after',
670
            $this->render(
671
                'before
672
<% loop Loop3 %>
673
$ItemOnItsOwnLine
674
<% end_loop %>
675
after'
676
            )
677
        );
678
    }
679
680
    public function testControls()
681
    {
682
        // Single item controls
683
        $this->assertEquals(
684
            'a[out:Foo.Bar.Item]b
685
				[out:Foo.Bar(Arg1).Item]
686
				[out:Foo(Arg1).Item]
687
				[out:Foo(Arg1,Arg2).Item]
688
				[out:Foo(Arg1,Arg2,Arg3).Item]',
689
            $this->render(
690
                '<% with Foo.Bar %>a{$Item}b<% end_with %>
691
				<% with Foo.Bar(Arg1) %>$Item<% end_with %>
692
				<% with Foo(Arg1) %>$Item<% end_with %>
693
				<% with Foo(Arg1, Arg2) %>$Item<% end_with %>
694
				<% with Foo(Arg1, Arg2, Arg3) %>$Item<% end_with %>'
695
            )
696
        );
697
698
        // Loop controls
699
        $this->assertEquals(
700
            'a[out:Foo.Loop2.Item]ba[out:Foo.Loop2.Item]b',
701
            $this->render('<% loop Foo.Loop2 %>a{$Item}b<% end_loop %>')
702
        );
703
704
        $this->assertEquals(
705
            '[out:Foo.Loop2(Arg1).Item][out:Foo.Loop2(Arg1).Item]',
706
            $this->render('<% loop Foo.Loop2(Arg1) %>$Item<% end_loop %>')
707
        );
708
709
        $this->assertEquals(
710
            '[out:Loop2(Arg1).Item][out:Loop2(Arg1).Item]',
711
            $this->render('<% loop Loop2(Arg1) %>$Item<% end_loop %>')
712
        );
713
714
        $this->assertEquals(
715
            '[out:Loop2(Arg1,Arg2).Item][out:Loop2(Arg1,Arg2).Item]',
716
            $this->render('<% loop Loop2(Arg1, Arg2) %>$Item<% end_loop %>')
717
        );
718
719
        $this->assertEquals(
720
            '[out:Loop2(Arg1,Arg2,Arg3).Item][out:Loop2(Arg1,Arg2,Arg3).Item]',
721
            $this->render('<% loop Loop2(Arg1, Arg2, Arg3) %>$Item<% end_loop %>')
722
        );
723
    }
724
725
    public function testIfBlocks()
726
    {
727
        // Basic test
728
        $this->assertEquals(
729
            'AC',
730
            $this->render('A<% if NotSet %>B$NotSet<% end_if %>C')
731
        );
732
733
        // Nested test
734
        $this->assertEquals(
735
            'AB1C',
736
            $this->render('A<% if IsSet %>B$NotSet<% if IsSet %>1<% else %>2<% end_if %><% end_if %>C')
737
        );
738
739
        // else_if
740
        $this->assertEquals(
741
            'ACD',
742
            $this->render('A<% if NotSet %>B<% else_if IsSet %>C<% end_if %>D')
743
        );
744
        $this->assertEquals(
745
            'AD',
746
            $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% end_if %>D')
747
        );
748
        $this->assertEquals(
749
            'ADE',
750
            $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E')
751
        );
752
753
        $this->assertEquals(
754
            'ADE',
755
            $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E')
756
        );
757
758
        // Dot syntax
759
        $this->assertEquals(
760
            'ACD',
761
            $this->render('A<% if Foo.NotSet %>B<% else_if Foo.IsSet %>C<% end_if %>D')
762
        );
763
        $this->assertEquals(
764
            'ACD',
765
            $this->render('A<% if Foo.Bar.NotSet %>B<% else_if Foo.Bar.IsSet %>C<% end_if %>D')
766
        );
767
768
        // Params
769
        $this->assertEquals(
770
            'ACD',
771
            $this->render('A<% if NotSet(Param) %>B<% else %>C<% end_if %>D')
772
        );
773
        $this->assertEquals(
774
            'ABD',
775
            $this->render('A<% if IsSet(Param) %>B<% else %>C<% end_if %>D')
776
        );
777
778
        // Negation
779
        $this->assertEquals(
780
            'AC',
781
            $this->render('A<% if not IsSet %>B<% end_if %>C')
782
        );
783
        $this->assertEquals(
784
            'ABC',
785
            $this->render('A<% if not NotSet %>B<% end_if %>C')
786
        );
787
788
        // Or
789
        $this->assertEquals(
790
            'ABD',
791
            $this->render('A<% if IsSet || NotSet %>B<% else_if A %>C<% end_if %>D')
792
        );
793
        $this->assertEquals(
794
            'ACD',
795
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet %>C<% end_if %>D')
796
        );
797
        $this->assertEquals(
798
            'AD',
799
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet3 %>C<% end_if %>D')
800
        );
801
        $this->assertEquals(
802
            'ACD',
803
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet || NotSet %>C<% end_if %>D')
804
        );
805
        $this->assertEquals(
806
            'AD',
807
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet2 || NotSet3 %>C<% end_if %>D')
808
        );
809
810
        // Negated Or
811
        $this->assertEquals(
812
            'ACD',
813
            $this->render('A<% if not IsSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D')
814
        );
815
        $this->assertEquals(
816
            'ABD',
817
            $this->render('A<% if not NotSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D')
818
        );
819
        $this->assertEquals(
820
            'ABD',
821
            $this->render('A<% if NotSet || not AlsoNotSet %>B<% else_if A %>C<% end_if %>D')
822
        );
823
824
        // And
825
        $this->assertEquals(
826
            'ABD',
827
            $this->render('A<% if IsSet && AlsoSet %>B<% else_if A %>C<% end_if %>D')
828
        );
829
        $this->assertEquals(
830
            'ACD',
831
            $this->render('A<% if IsSet && NotSet %>B<% else_if IsSet %>C<% end_if %>D')
832
        );
833
        $this->assertEquals(
834
            'AD',
835
            $this->render('A<% if NotSet && NotSet2 %>B<% else_if NotSet3 %>C<% end_if %>D')
836
        );
837
        $this->assertEquals(
838
            'ACD',
839
            $this->render('A<% if IsSet && NotSet %>B<% else_if IsSet && AlsoSet %>C<% end_if %>D')
840
        );
841
        $this->assertEquals(
842
            'AD',
843
            $this->render('A<% if NotSet && NotSet2 %>B<% else_if IsSet && NotSet3 %>C<% end_if %>D')
844
        );
845
846
        // Equality
847
        $this->assertEquals(
848
            'ABC',
849
            $this->render('A<% if RawVal == RawVal %>B<% end_if %>C')
850
        );
851
        $this->assertEquals(
852
            'ACD',
853
            $this->render('A<% if Right == Wrong %>B<% else_if RawVal == RawVal %>C<% end_if %>D')
854
        );
855
        $this->assertEquals(
856
            'ABC',
857
            $this->render('A<% if Right != Wrong %>B<% end_if %>C')
858
        );
859
        $this->assertEquals(
860
            'AD',
861
            $this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% end_if %>D')
862
        );
863
864
        // test inequalities with simple numbers
865
        $this->assertEquals('ABD', $this->render('A<% if 5 > 3 %>B<% else %>C<% end_if %>D'));
866
        $this->assertEquals('ABD', $this->render('A<% if 5 >= 3 %>B<% else %>C<% end_if %>D'));
867
        $this->assertEquals('ACD', $this->render('A<% if 3 > 5 %>B<% else %>C<% end_if %>D'));
868
        $this->assertEquals('ACD', $this->render('A<% if 3 >= 5 %>B<% else %>C<% end_if %>D'));
869
870
        $this->assertEquals('ABD', $this->render('A<% if 3 < 5 %>B<% else %>C<% end_if %>D'));
871
        $this->assertEquals('ABD', $this->render('A<% if 3 <= 5 %>B<% else %>C<% end_if %>D'));
872
        $this->assertEquals('ACD', $this->render('A<% if 5 < 3 %>B<% else %>C<% end_if %>D'));
873
        $this->assertEquals('ACD', $this->render('A<% if 5 <= 3 %>B<% else %>C<% end_if %>D'));
874
875
        $this->assertEquals('ABD', $this->render('A<% if 4 <= 4 %>B<% else %>C<% end_if %>D'));
876
        $this->assertEquals('ABD', $this->render('A<% if 4 >= 4 %>B<% else %>C<% end_if %>D'));
877
        $this->assertEquals('ACD', $this->render('A<% if 4 > 4 %>B<% else %>C<% end_if %>D'));
878
        $this->assertEquals('ACD', $this->render('A<% if 4 < 4 %>B<% else %>C<% end_if %>D'));
879
880
        // empty else_if and else tags, if this would not be supported,
881
        // the output would stop after A, thereby failing the assert
882
        $this->assertEquals('AD', $this->render('A<% if IsSet %><% else %><% end_if %>D'));
883
        $this->assertEquals(
884
            'AD',
885
            $this->render('A<% if NotSet %><% else_if IsSet %><% else %><% end_if %>D')
886
        );
887
        $this->assertEquals(
888
            'AD',
889
            $this->render('A<% if NotSet %><% else_if AlsoNotSet %><% else %><% end_if %>D')
890
        );
891
892
        // Bare words with ending space
893
        $this->assertEquals(
894
            'ABC',
895
            $this->render('A<% if "RawVal" == RawVal %>B<% end_if %>C')
896
        );
897
898
        // Else
899
        $this->assertEquals(
900
            'ADE',
901
            $this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% else %>D<% end_if %>E')
902
        );
903
904
        // Empty if with else
905
        $this->assertEquals(
906
            'ABC',
907
            $this->render('A<% if NotSet %><% else %>B<% end_if %>C')
908
        );
909
    }
910
911
    public function testBaseTagGeneration()
912
    {
913
        // XHTML wil have a closed base tag
914
        $tmpl1 = '<?xml version="1.0" encoding="UTF-8"?>
915
			<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
916
            . ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
917
			<html>
918
				<head><% base_tag %></head>
919
				<body><p>test</p><body>
920
			</html>';
921
        $this->assertRegExp('/<head><base href=".*" \/><\/head>/', $this->render($tmpl1));
922
923
        // HTML4 and 5 will only have it for IE
924
        $tmpl2 = '<!DOCTYPE html>
925
			<html>
926
				<head><% base_tag %></head>
927
				<body><p>test</p><body>
928
			</html>';
929
        $this->assertRegExp(
930
            '/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
931
            $this->render($tmpl2)
932
        );
933
934
935
        $tmpl3 = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
936
			<html>
937
				<head><% base_tag %></head>
938
				<body><p>test</p><body>
939
			</html>';
940
        $this->assertRegExp(
941
            '/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
942
            $this->render($tmpl3)
943
        );
944
945
        // Check that the content negotiator converts to the equally legal formats
946
        $negotiator = new ContentNegotiator();
947
948
        $response = new HTTPResponse($this->render($tmpl1));
949
        $negotiator->html($response);
950
        $this->assertRegExp(
951
            '/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
952
            $response->getBody()
953
        );
954
955
        $response = new HTTPResponse($this->render($tmpl1));
956
        $negotiator->xhtml($response);
957
        $this->assertRegExp('/<head><base href=".*" \/><\/head>/', $response->getBody());
958
    }
959
960
    public function testIncludeWithArguments()
961
    {
962
        $this->assertEquals(
963
            $this->render('<% include SSViewerTestIncludeWithArguments %>'),
964
            '<p>[out:Arg1]</p><p>[out:Arg2]</p>'
965
        );
966
967
        $this->assertEquals(
968
            $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A %>'),
969
            '<p>A</p><p>[out:Arg2]</p>'
970
        );
971
972
        $this->assertEquals(
973
            $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A, Arg2=B %>'),
974
            '<p>A</p><p>B</p>'
975
        );
976
977
        $this->assertEquals(
978
            $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>'),
979
            '<p>A Bare String</p><p>B Bare String</p>'
980
        );
981
982
        $this->assertEquals(
983
            $this->render(
984
                '<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=$B %>',
985
                new ArrayData(array('B' => 'Bar'))
986
            ),
987
            '<p>A</p><p>Bar</p>'
988
        );
989
990
        $this->assertEquals(
991
            $this->render(
992
                '<% include SSViewerTestIncludeWithArguments Arg1="A" %>',
993
                new ArrayData(array('Arg1' => 'Foo', 'Arg2' => 'Bar'))
994
            ),
995
            '<p>A</p><p>Bar</p>'
996
        );
997
998
        $this->assertEquals(
999
            $this->render(
1000
                '<% include SSViewerTestIncludeScopeInheritanceWithArgsInLoop Title="SomeArg" %>',
1001
                new ArrayData(
1002
                    array('Items' => new ArrayList(
1003
                        array(
1004
                        new ArrayData(array('Title' => 'Foo')),
1005
                        new ArrayData(array('Title' => 'Bar'))
1006
                        )
1007
                    ))
1008
                )
1009
            ),
1010
            'SomeArg - Foo - Bar - SomeArg'
1011
        );
1012
1013
        $this->assertEquals(
1014
            $this->render(
1015
                '<% include SSViewerTestIncludeScopeInheritanceWithArgsInWith Title="A" %>',
1016
                new ArrayData(array('Item' => new ArrayData(array('Title' =>'B'))))
1017
            ),
1018
            'A - B - A'
1019
        );
1020
1021
        $this->assertEquals(
1022
            $this->render(
1023
                '<% include SSViewerTestIncludeScopeInheritanceWithArgsInNestedWith Title="A" %>',
1024
                new ArrayData(
1025
                    array(
1026
                    'Item' => new ArrayData(
1027
                        array(
1028
                        'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
1029
                        )
1030
                    ))
1031
                )
1032
            ),
1033
            'A - B - C - B - A'
1034
        );
1035
1036
        $this->assertEquals(
1037
            $this->render(
1038
                '<% include SSViewerTestIncludeScopeInheritanceWithUpAndTop Title="A" %>',
1039
                new ArrayData(
1040
                    array(
1041
                    'Item' => new ArrayData(
1042
                        array(
1043
                        'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
1044
                        )
1045
                    ))
1046
                )
1047
            ),
1048
            'A - A - A'
1049
        );
1050
1051
        $data = new ArrayData(
1052
            array(
1053
            'Nested' => new ArrayData(
1054
                array(
1055
                'Object' => new ArrayData(array('Key' => 'A'))
1056
                )
1057
            ),
1058
            'Object' => new ArrayData(array('Key' => 'B'))
1059
            )
1060
        );
1061
1062
        $tmpl = SSViewer::fromString('<% include SSViewerTestIncludeObjectArguments A=$Nested.Object, B=$Object %>');
1063
        $res  = $tmpl->process($data);
1064
        $this->assertEqualIgnoringWhitespace('A B', $res, 'Objects can be passed as named arguments');
1065
    }
1066
1067
    public function testNamespaceInclude()
1068
    {
1069
        $data = new ArrayData([]);
1070
1071
        $this->assertEquals(
1072
            "tests:( NamespaceInclude\n )",
1073
            $this->render('tests:( <% include Namespace\NamespaceInclude %> )', $data),
1074
            'Backslashes work for namespace references in includes'
1075
        );
1076
1077
        $this->assertEquals(
1078
            "tests:( NamespaceInclude\n )",
1079
            $this->render('tests:( <% include Namespace\\NamespaceInclude %> )', $data),
1080
            'Escaped backslashes work for namespace references in includes'
1081
        );
1082
1083
        $this->assertEquals(
1084
            "tests:( NamespaceInclude\n )",
1085
            $this->render('tests:( <% include Namespace/NamespaceInclude %> )', $data),
1086
            'Forward slashes work for namespace references in includes'
1087
        );
1088
    }
1089
1090
    /**
1091
     * Test search for includes fallback to non-includes folder
1092
     */
1093
    public function testIncludeFallbacks()
1094
    {
1095
        $data = new ArrayData([]);
1096
1097
        $this->assertEquals(
1098
            "tests:( Namespace/Includes/IncludedTwice.ss\n )",
1099
            $this->render('tests:( <% include Namespace\\IncludedTwice %> )', $data),
1100
            'Prefer Includes in the Includes folder'
1101
        );
1102
1103
        $this->assertEquals(
1104
            "tests:( Namespace/Includes/IncludedOnceSub.ss\n )",
1105
            $this->render('tests:( <% include Namespace\\IncludedOnceSub %> )', $data),
1106
            'Includes in only Includes folder can be found'
1107
        );
1108
1109
        $this->assertEquals(
1110
            "tests:( Namespace/IncludedOnceBase.ss\n )",
1111
            $this->render('tests:( <% include Namespace\\IncludedOnceBase %> )', $data),
1112
            'Includes outside of Includes folder can be found'
1113
        );
1114
    }
1115
1116
    public function testRecursiveInclude()
1117
    {
1118
        $view = new SSViewer(array('Includes/SSViewerTestRecursiveInclude'));
1119
1120
        $data = new ArrayData(
1121
            array(
1122
            'Title' => 'A',
1123
            'Children' => new ArrayList(
1124
                array(
1125
                new ArrayData(
1126
                    array(
1127
                    'Title' => 'A1',
1128
                    'Children' => new ArrayList(
1129
                        array(
1130
                        new ArrayData(array( 'Title' => 'A1 i', )),
1131
                        new ArrayData(array( 'Title' => 'A1 ii', )),
1132
                        )
1133
                    ),
1134
                    )
1135
                ),
1136
                new ArrayData(array( 'Title' => 'A2', )),
1137
                new ArrayData(array( 'Title' => 'A3', )),
1138
                )
1139
            ),
1140
            )
1141
        );
1142
1143
        $result = $view->process($data);
1144
        // We don't care about whitespace
1145
        $rationalisedResult = trim(preg_replace('/\s+/', ' ', $result));
1146
1147
        $this->assertEquals('A A1 A1 i A1 ii A2 A3', $rationalisedResult);
1148
    }
1149
1150
    public function assertEqualIgnoringWhitespace($a, $b, $message = '')
1151
    {
1152
        $this->assertEquals(preg_replace('/\s+/', '', $a), preg_replace('/\s+/', '', $b), $message);
1153
    }
1154
1155
    /**
1156
     * See {@link ViewableDataTest} for more extensive casting tests,
1157
     * this test just ensures that basic casting is correctly applied during template parsing.
1158
     */
1159
    public function testCastingHelpers()
1160
    {
1161
        $vd = new SSViewerTest\TestViewableData();
1162
        $vd->TextValue = '<b>html</b>';
1163
        $vd->HTMLValue = '<b>html</b>';
1164
        $vd->UncastedValue = '<b>html</b>';
1165
1166
        // Value casted as "Text"
1167
        $this->assertEquals(
1168
            '&lt;b&gt;html&lt;/b&gt;',
1169
            $t = SSViewer::fromString('$TextValue')->process($vd)
1170
        );
1171
        $this->assertEquals(
1172
            '<b>html</b>',
1173
            $t = SSViewer::fromString('$TextValue.RAW')->process($vd)
1174
        );
1175
        $this->assertEquals(
1176
            '&lt;b&gt;html&lt;/b&gt;',
1177
            $t = SSViewer::fromString('$TextValue.XML')->process($vd)
1178
        );
1179
1180
        // Value casted as "HTMLText"
1181
        $this->assertEquals(
1182
            '<b>html</b>',
1183
            $t = SSViewer::fromString('$HTMLValue')->process($vd)
1184
        );
1185
        $this->assertEquals(
1186
            '<b>html</b>',
1187
            $t = SSViewer::fromString('$HTMLValue.RAW')->process($vd)
1188
        );
1189
        $this->assertEquals(
1190
            '&lt;b&gt;html&lt;/b&gt;',
1191
            $t = SSViewer::fromString('$HTMLValue.XML')->process($vd)
1192
        );
1193
1194
        // Uncasted value (falls back to ViewableData::$default_cast="Text")
1195
        $vd = new SSViewerTest\TestViewableData();
1196
        $vd->UncastedValue = '<b>html</b>';
1197
        $this->assertEquals(
1198
            '&lt;b&gt;html&lt;/b&gt;',
1199
            $t = SSViewer::fromString('$UncastedValue')->process($vd)
1200
        );
1201
        $this->assertEquals(
1202
            '<b>html</b>',
1203
            $t = SSViewer::fromString('$UncastedValue.RAW')->process($vd)
1204
        );
1205
        $this->assertEquals(
1206
            '&lt;b&gt;html&lt;/b&gt;',
1207
            $t = SSViewer::fromString('$UncastedValue.XML')->process($vd)
1208
        );
1209
    }
1210
1211
    public function testSSViewerBasicIteratorSupport()
1212
    {
1213
        $data = new ArrayData(
1214
            array(
1215
            'Set' => new ArrayList(
1216
                array(
1217
                new SSViewerTest\TestObject("1"),
1218
                new SSViewerTest\TestObject("2"),
1219
                new SSViewerTest\TestObject("3"),
1220
                new SSViewerTest\TestObject("4"),
1221
                new SSViewerTest\TestObject("5"),
1222
                new SSViewerTest\TestObject("6"),
1223
                new SSViewerTest\TestObject("7"),
1224
                new SSViewerTest\TestObject("8"),
1225
                new SSViewerTest\TestObject("9"),
1226
                new SSViewerTest\TestObject("10"),
1227
                )
1228
            )
1229
            )
1230
        );
1231
1232
        //base test
1233
        $result = $this->render('<% loop Set %>$Number<% end_loop %>', $data);
1234
        $this->assertEquals("12345678910", $result, "Numbers rendered in order");
1235
1236
        //test First
1237
        $result = $this->render('<% loop Set %><% if First %>$Number<% end_if %><% end_loop %>', $data);
1238
        $this->assertEquals("1", $result, "Only the first number is rendered");
1239
1240
        //test Last
1241
        $result = $this->render('<% loop Set %><% if Last %>$Number<% end_if %><% end_loop %>', $data);
1242
        $this->assertEquals("10", $result, "Only the last number is rendered");
1243
1244
        //test Even
1245
        $result = $this->render('<% loop Set %><% if Even() %>$Number<% end_if %><% end_loop %>', $data);
1246
        $this->assertEquals("246810", $result, "Even numbers rendered in order");
1247
1248
        //test Even with quotes
1249
        $result = $this->render('<% loop Set %><% if Even("1") %>$Number<% end_if %><% end_loop %>', $data);
1250
        $this->assertEquals("246810", $result, "Even numbers rendered in order");
1251
1252
        //test Even without quotes
1253
        $result = $this->render('<% loop Set %><% if Even(1) %>$Number<% end_if %><% end_loop %>', $data);
1254
        $this->assertEquals("246810", $result, "Even numbers rendered in order");
1255
1256
        //test Even with zero-based start index
1257
        $result = $this->render('<% loop Set %><% if Even("0") %>$Number<% end_if %><% end_loop %>', $data);
1258
        $this->assertEquals("13579", $result, "Even (with zero-based index) numbers rendered in order");
1259
1260
        //test Odd
1261
        $result = $this->render('<% loop Set %><% if Odd %>$Number<% end_if %><% end_loop %>', $data);
1262
        $this->assertEquals("13579", $result, "Odd numbers rendered in order");
1263
1264
        //test FirstLast
1265
        $result = $this->render('<% loop Set %><% if FirstLast %>$Number$FirstLast<% end_if %><% end_loop %>', $data);
1266
        $this->assertEquals("1first10last", $result, "First and last numbers rendered in order");
1267
1268
        //test Middle
1269
        $result = $this->render('<% loop Set %><% if Middle %>$Number<% end_if %><% end_loop %>', $data);
1270
        $this->assertEquals("23456789", $result, "Middle numbers rendered in order");
1271
1272
        //test MiddleString
1273
        $result = $this->render(
1274
            '<% loop Set %><% if MiddleString == "middle" %>$Number$MiddleString<% end_if %>'
1275
            . '<% end_loop %>',
1276
            $data
1277
        );
1278
        $this->assertEquals(
1279
            "2middle3middle4middle5middle6middle7middle8middle9middle",
1280
            $result,
1281
            "Middle numbers rendered in order"
1282
        );
1283
1284
        //test EvenOdd
1285
        $result = $this->render('<% loop Set %>$EvenOdd<% end_loop %>', $data);
1286
        $this->assertEquals(
1287
            "oddevenoddevenoddevenoddevenoddeven",
1288
            $result,
1289
            "Even and Odd is returned in sequence numbers rendered in order"
1290
        );
1291
1292
        //test Pos
1293
        $result = $this->render('<% loop Set %>$Pos<% end_loop %>', $data);
1294
        $this->assertEquals("12345678910", $result, '$Pos is rendered in order');
1295
1296
        //test Pos
1297
        $result = $this->render('<% loop Set %>$Pos(0)<% end_loop %>', $data);
1298
        $this->assertEquals("0123456789", $result, '$Pos(0) is rendered in order');
1299
1300
        //test FromEnd
1301
        $result = $this->render('<% loop Set %>$FromEnd<% end_loop %>', $data);
1302
        $this->assertEquals("10987654321", $result, '$FromEnd is rendered in order');
1303
1304
        //test FromEnd
1305
        $result = $this->render('<% loop Set %>$FromEnd(0)<% end_loop %>', $data);
1306
        $this->assertEquals("9876543210", $result, '$FromEnd(0) rendered in order');
1307
1308
        //test Total
1309
        $result = $this->render('<% loop Set %>$TotalItems<% end_loop %>', $data);
1310
        $this->assertEquals("10101010101010101010", $result, "10 total items X 10 are returned");
1311
1312
        //test Modulus
1313
        $result = $this->render('<% loop Set %>$Modulus(2,1)<% end_loop %>', $data);
1314
        $this->assertEquals("1010101010", $result, "1-indexed pos modular divided by 2 rendered in order");
1315
1316
        //test MultipleOf 3
1317
        $result = $this->render('<% loop Set %><% if MultipleOf(3) %>$Number<% end_if %><% end_loop %>', $data);
1318
        $this->assertEquals("369", $result, "Only numbers that are multiples of 3 are returned");
1319
1320
        //test MultipleOf 4
1321
        $result = $this->render('<% loop Set %><% if MultipleOf(4) %>$Number<% end_if %><% end_loop %>', $data);
1322
        $this->assertEquals("48", $result, "Only numbers that are multiples of 4 are returned");
1323
1324
        //test MultipleOf 5
1325
        $result = $this->render('<% loop Set %><% if MultipleOf(5) %>$Number<% end_if %><% end_loop %>', $data);
1326
        $this->assertEquals("510", $result, "Only numbers that are multiples of 5 are returned");
1327
1328
        //test MultipleOf 10
1329
        $result = $this->render('<% loop Set %><% if MultipleOf(10,1) %>$Number<% end_if %><% end_loop %>', $data);
1330
        $this->assertEquals("10", $result, "Only numbers that are multiples of 10 (with 1-based indexing) are returned");
1331
1332
        //test MultipleOf 9 zero-based
1333
        $result = $this->render('<% loop Set %><% if MultipleOf(9,0) %>$Number<% end_if %><% end_loop %>', $data);
1334
        $this->assertEquals(
1335
            "110",
1336
            $result,
1337
            "Only numbers that are multiples of 9 with zero-based indexing are returned. (The first and last item)"
1338
        );
1339
1340
        //test MultipleOf 11
1341
        $result = $this->render('<% loop Set %><% if MultipleOf(11) %>$Number<% end_if %><% end_loop %>', $data);
1342
        $this->assertEquals("", $result, "Only numbers that are multiples of 11 are returned. I.e. nothing returned");
1343
    }
1344
1345
    /**
1346
     * Test $Up works when the scope $Up refers to was entered with a "with" block
1347
     */
1348
    public function testUpInWith()
1349
    {
1350
1351
        // Data to run the loop tests on - three levels deep
1352
        $data = new ArrayData(
1353
            array(
1354
            'Name' => 'Top',
1355
            'Foo' => new ArrayData(
1356
                array(
1357
                'Name' => 'Foo',
1358
                'Bar' => new ArrayData(
1359
                    array(
1360
                    'Name' => 'Bar',
1361
                    'Baz' => new ArrayData(
1362
                        array(
1363
                        'Name' => 'Baz'
1364
                        )
1365
                    ),
1366
                    'Qux' => new ArrayData(
1367
                        array(
1368
                        'Name' => 'Qux'
1369
                        )
1370
                    )
1371
                    )
1372
                )
1373
                )
1374
            )
1375
            )
1376
        );
1377
1378
        // Basic functionality
1379
        $this->assertEquals(
1380
            'BarFoo',
1381
            $this->render('<% with Foo %><% with Bar %>{$Name}{$Up.Name}<% end_with %><% end_with %>', $data)
1382
        );
1383
1384
        // Two level with block, up refers to internally referenced Bar
1385
        $this->assertEquals(
1386
            'BarFoo',
1387
            $this->render('<% with Foo.Bar %>{$Name}{$Up.Name}<% end_with %>', $data)
1388
        );
1389
1390
        // Stepping up & back down the scope tree
1391
        $this->assertEquals(
1392
            'BazBarQux',
1393
            $this->render('<% with Foo.Bar.Baz %>{$Name}{$Up.Name}{$Up.Qux.Name}<% end_with %>', $data)
1394
        );
1395
1396
        // Using $Up in a with block
1397
        $this->assertEquals(
1398
            'BazBarQux',
1399
            $this->render(
1400
                '<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}{$Qux.Name}<% end_with %>'
1401
                .'<% end_with %>',
1402
                $data
1403
            )
1404
        );
1405
1406
        // Stepping up & back down the scope tree with with blocks
1407
        $this->assertEquals(
1408
            'BazBarQuxBarBaz',
1409
            $this->render(
1410
                '<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}<% with Qux %>{$Name}<% end_with %>'
1411
                . '{$Name}<% end_with %>{$Name}<% end_with %>',
1412
                $data
1413
            )
1414
        );
1415
1416
        // Using $Up.Up, where first $Up points to a previous scope entered using $Up, thereby skipping up to Foo
1417
        $this->assertEquals(
1418
            'Foo',
1419
            $this->render(
1420
                '<% with Foo.Bar.Baz %><% with Up %><% with Qux %>{$Up.Up.Name}<% end_with %><% end_with %>'
1421
                . '<% end_with %>',
1422
                $data
1423
            )
1424
        );
1425
1426
        // Using $Up.Up, where first $Up points to an Up used in a local scope lookup, should still skip to Foo
1427
        $this->assertEquals(
1428
            'Foo',
1429
            $this->render('<% with Foo.Bar.Baz.Up.Qux %>{$Up.Up.Name}<% end_with %>', $data)
1430
        );
1431
    }
1432
1433
    /**
1434
     * Test $Up works when the scope $Up refers to was entered with a "loop" block
1435
     */
1436
    public function testUpInLoop()
1437
    {
1438
1439
        // Data to run the loop tests on - one sequence of three items, each with a subitem
1440
        $data = new ArrayData(
1441
            array(
1442
            'Name' => 'Top',
1443
            'Foo' => new ArrayList(
1444
                array(
1445
                new ArrayData(
1446
                    array(
1447
                    'Name' => '1',
1448
                    'Sub' => new ArrayData(
1449
                        array(
1450
                        'Name' => 'Bar'
1451
                        )
1452
                    )
1453
                    )
1454
                ),
1455
                new ArrayData(
1456
                    array(
1457
                    'Name' => '2',
1458
                    'Sub' => new ArrayData(
1459
                        array(
1460
                        'Name' => 'Baz'
1461
                        )
1462
                    )
1463
                    )
1464
                ),
1465
                new ArrayData(
1466
                    array(
1467
                    'Name' => '3',
1468
                    'Sub' => new ArrayData(
1469
                        array(
1470
                        'Name' => 'Qux'
1471
                        )
1472
                    )
1473
                    )
1474
                )
1475
                )
1476
            )
1477
            )
1478
        );
1479
1480
        // Make sure inside a loop, $Up refers to the current item of the loop
1481
        $this->assertEqualIgnoringWhitespace(
1482
            '111 222 333',
1483
            $this->render(
1484
                '<% loop $Foo %>$Name<% with $Sub %>$Up.Name<% end_with %>$Name<% end_loop %>',
1485
                $data
1486
            )
1487
        );
1488
1489
        // Make sure inside a loop, looping over $Up uses a separate iterator,
1490
        // and doesn't interfere with the original iterator
1491
        $this->assertEqualIgnoringWhitespace(
1492
            '1Bar123Bar1 2Baz123Baz2 3Qux123Qux3',
1493
            $this->render(
1494
                '<% loop $Foo %>
1495
					$Name
1496
					<% with $Sub %>
1497
						$Name
1498
						<% loop $Up %>$Name<% end_loop %>
1499
						$Name
1500
					<% end_with %>
1501
					$Name
1502
				<% end_loop %>',
1503
                $data
1504
            )
1505
        );
1506
1507
        // Make sure inside a loop, looping over $Up uses a separate iterator,
1508
        // and doesn't interfere with the original iterator or local lookups
1509
        $this->assertEqualIgnoringWhitespace(
1510
            '1 Bar1 123 1Bar 1   2 Baz2 123 2Baz 2   3 Qux3 123 3Qux 3',
1511
            $this->render(
1512
                '<% loop $Foo %>
1513
					$Name
1514
					<% with $Sub %>
1515
						{$Name}{$Up.Name}
1516
						<% loop $Up %>$Name<% end_loop %>
1517
						{$Up.Name}{$Name}
1518
					<% end_with %>
1519
					$Name
1520
				<% end_loop %>',
1521
                $data
1522
            )
1523
        );
1524
    }
1525
1526
    /**
1527
     * Test that nested loops restore the loop variables correctly when pushing and popping states
1528
     */
1529
    public function testNestedLoops()
1530
    {
1531
1532
        // Data to run the loop tests on - one sequence of three items, one with child elements
1533
        // (of a different size to the main sequence)
1534
        $data = new ArrayData(
1535
            array(
1536
            'Foo' => new ArrayList(
1537
                array(
1538
                new ArrayData(
1539
                    array(
1540
                    'Name' => '1',
1541
                    'Children' => new ArrayList(
1542
                        array(
1543
                        new ArrayData(
1544
                            array(
1545
                            'Name' => 'a'
1546
                            )
1547
                        ),
1548
                        new ArrayData(
1549
                            array(
1550
                            'Name' => 'b'
1551
                            )
1552
                        ),
1553
                        )
1554
                    ),
1555
                    )
1556
                ),
1557
                new ArrayData(
1558
                    array(
1559
                    'Name' => '2',
1560
                    'Children' => new ArrayList(),
1561
                    )
1562
                ),
1563
                new ArrayData(
1564
                    array(
1565
                    'Name' => '3',
1566
                    'Children' => new ArrayList(),
1567
                    )
1568
                ),
1569
                )
1570
            ),
1571
            )
1572
        );
1573
1574
        // Make sure that including a loop inside a loop will not destroy the internal count of
1575
        // items, checked by using "Last"
1576
        $this->assertEqualIgnoringWhitespace(
1577
            '1ab23last',
1578
            $this->render(
1579
                '<% loop $Foo %>$Name<% loop Children %>$Name<% end_loop %><% if Last %>last<% end_if %>'
1580
                . '<% end_loop %>',
1581
                $data
1582
            )
1583
        );
1584
    }
1585
1586
    public function testLayout()
1587
    {
1588
        $this->useTestTheme(
1589
            __DIR__.'/SSViewerTest',
1590
            'layouttest',
1591
            function () {
1592
                $template = new SSViewer(array('Page'));
1593
                $this->assertEquals("Foo\n\n", $template->process(new ArrayData(array())));
1594
1595
                $template = new SSViewer(array('Shortcodes', 'Page'));
1596
                $this->assertEquals("[file_link]\n\n", $template->process(new ArrayData(array())));
1597
            }
1598
        );
1599
    }
1600
1601
    /**
1602
     * @covers \SilverStripe\View\SSViewer::get_templates_by_class()
1603
     */
1604
    public function testGetTemplatesByClass()
1605
    {
1606
        $this->useTestTheme(
1607
            __DIR__ . '/SSViewerTest',
1608
            'layouttest',
1609
            function () {
1610
            // Test passing a string
1611
                $templates = SSViewer::get_templates_by_class(
1612
                    SSViewerTestModelController::class,
1613
                    '',
1614
                    Controller::class
1615
                );
1616
                $this->assertEquals(
1617
                    [
1618
                    SSViewerTestModelController::class,
1619
                    [
1620
                        'type' => 'Includes',
1621
                        SSViewerTestModelController::class,
1622
                    ],
1623
                    SSViewerTestModel::class,
1624
                    Controller::class,
1625
                    [
1626
                        'type' => 'Includes',
1627
                        Controller::class,
1628
                    ],
1629
                    ],
1630
                    $templates
1631
                );
1632
1633
            // Test to ensure we're stopping at the base class.
1634
                $templates = SSViewer::get_templates_by_class(
1635
                    SSViewerTestModelController::class,
1636
                    '',
1637
                    SSViewerTestModelController::class
1638
                );
1639
                $this->assertEquals(
1640
                    [
1641
                    SSViewerTestModelController::class,
1642
                    [
1643
                        'type' => 'Includes',
1644
                        SSViewerTestModelController::class,
1645
                    ],
1646
                    SSViewerTestModel::class,
1647
                    ],
1648
                    $templates
1649
                );
1650
1651
            // Make sure we can search templates by suffix.
1652
                $templates = SSViewer::get_templates_by_class(
1653
                    SSViewerTestModel::class,
1654
                    'Controller',
1655
                    DataObject::class
1656
                );
1657
                $this->assertEquals(
1658
                    [
1659
                    SSViewerTestModelController::class,
1660
                    [
1661
                        'type' => 'Includes',
1662
                        SSViewerTestModelController::class,
1663
                    ],
1664
                    DataObject::class . 'Controller',
1665
                    [
1666
                        'type' => 'Includes',
1667
                        DataObject::class . 'Controller',
1668
                    ],
1669
                    ],
1670
                    $templates
1671
                );
1672
1673
                // Let's throw something random in there.
1674
                $this->expectException(InvalidArgumentException::class);
1675
                SSViewer::get_templates_by_class(null);
1676
            }
1677
        );
1678
    }
1679
1680
    public function testRewriteHashlinks()
1681
    {
1682
        SSViewer::setRewriteHashLinksDefault(true);
1683
1684
        $_SERVER['HTTP_HOST'] = 'www.mysite.com';
1685
        $_SERVER['REQUEST_URI'] = '//file.com?foo"onclick="alert(\'xss\')""';
1686
1687
        // Emulate SSViewer::process()
1688
        // Note that leading double slashes have been rewritten to prevent these being mis-interepreted
1689
        // as protocol-less absolute urls
1690
        $base = Convert::raw2att('/file.com?foo"onclick="alert(\'xss\')""');
1691
1692
        $tmplFile = TEMP_PATH . DIRECTORY_SEPARATOR . 'SSViewerTest_testRewriteHashlinks_' . sha1(rand()) . '.ss';
0 ignored issues
show
The call to rand() has too few arguments starting with min. ( Ignorable by Annotation )

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

1692
        $tmplFile = TEMP_PATH . DIRECTORY_SEPARATOR . 'SSViewerTest_testRewriteHashlinks_' . sha1(/** @scrutinizer ignore-call */ rand()) . '.ss';

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1693
1694
        // Note: SSViewer_FromString doesn't rewrite hash links.
1695
        file_put_contents(
1696
            $tmplFile,
1697
            '<!DOCTYPE html>
1698
			<html>
1699
				<head><% base_tag %></head>
1700
				<body>
1701
				<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>
1702
				$ExternalInsertedLink
1703
				<a class="inline" href="#anchor">InlineLink</a>
1704
				$InsertedLink
1705
				<svg><use xlink:href="#sprite"></use></svg>
1706
				<body>
1707
			</html>'
1708
        );
1709
        $tmpl = new SSViewer($tmplFile);
1710
        $obj = new ViewableData();
1711
        $obj->InsertedLink = DBField::create_field(
1712
            'HTMLFragment',
1713
            '<a class="inserted" href="#anchor">InsertedLink</a>'
1714
        );
1715
        $obj->ExternalInsertedLink = DBField::create_field(
1716
            'HTMLFragment',
1717
            '<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>'
1718
        );
1719
        $result = $tmpl->process($obj);
1720
        $this->assertContains(
1721
            '<a class="inserted" href="' . $base . '#anchor">InsertedLink</a>',
1722
            $result
1723
        );
1724
        $this->assertContains(
1725
            '<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>',
1726
            $result
1727
        );
1728
        $this->assertContains(
1729
            '<a class="inline" href="' . $base . '#anchor">InlineLink</a>',
1730
            $result
1731
        );
1732
        $this->assertContains(
1733
            '<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>',
1734
            $result
1735
        );
1736
        $this->assertContains(
1737
            '<svg><use xlink:href="#sprite"></use></svg>',
1738
            $result,
1739
            'SSTemplateParser should only rewrite anchor hrefs'
1740
        );
1741
1742
        unlink($tmplFile);
1743
    }
1744
1745
    public function testRewriteHashlinksInPhpMode()
1746
    {
1747
        SSViewer::setRewriteHashLinksDefault('php');
1748
1749
        $tmplFile = TEMP_PATH . DIRECTORY_SEPARATOR . 'SSViewerTest_testRewriteHashlinksInPhpMode_' . sha1(rand()) . '.ss';
0 ignored issues
show
The call to rand() has too few arguments starting with min. ( Ignorable by Annotation )

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

1749
        $tmplFile = TEMP_PATH . DIRECTORY_SEPARATOR . 'SSViewerTest_testRewriteHashlinksInPhpMode_' . sha1(/** @scrutinizer ignore-call */ rand()) . '.ss';

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1750
1751
        // Note: SSViewer_FromString doesn't rewrite hash links.
1752
        file_put_contents(
1753
            $tmplFile,
1754
            '<!DOCTYPE html>
1755
			<html>
1756
				<head><% base_tag %></head>
1757
				<body>
1758
				<a class="inline" href="#anchor">InlineLink</a>
1759
				$InsertedLink
1760
				<svg><use xlink:href="#sprite"></use></svg>
1761
				<body>
1762
			</html>'
1763
        );
1764
        $tmpl = new SSViewer($tmplFile);
1765
        $obj = new ViewableData();
1766
        $obj->InsertedLink = DBField::create_field(
1767
            'HTMLFragment',
1768
            '<a class="inserted" href="#anchor">InsertedLink</a>'
1769
        );
1770
        $result = $tmpl->process($obj);
1771
1772
        $code = <<<'EOC'
1773
<a class="inserted" href="<?php echo \SilverStripe\Core\Convert::raw2att(preg_replace("/^(\/)+/", "/", $_SERVER['REQUEST_URI'])); ?>#anchor">InsertedLink</a>
1774
EOC;
1775
        $this->assertContains($code, $result);
1776
        // TODO Fix inline links in PHP mode
1777
        // $this->assertContains(
1778
        // 	'<a class="inline" href="<?php echo str_replace(',
1779
        // 	$result
1780
        // );
1781
        $this->assertContains(
1782
            '<svg><use xlink:href="#sprite"></use></svg>',
1783
            $result,
1784
            'SSTemplateParser should only rewrite anchor hrefs'
1785
        );
1786
1787
        unlink($tmplFile);
1788
    }
1789
1790
    public function testRenderWithSourceFileComments()
1791
    {
1792
        SSViewer::config()->update('source_file_comments', true);
1793
        $i = __DIR__ . '/SSViewerTest/templates/Includes';
1794
        $f = __DIR__ . '/SSViewerTest/templates/SSViewerTestComments';
1795
        $templates = array(
1796
        array(
1797
            'name' => 'SSViewerTestCommentsFullSource',
1798
            'expected' => ""
1799
                . "<!doctype html>"
1800
                . "<!-- template $f/SSViewerTestCommentsFullSource.ss -->"
1801
                . "<html>"
1802
                . "\t<head></head>"
1803
                . "\t<body></body>"
1804
                . "</html>"
1805
                . "<!-- end template $f/SSViewerTestCommentsFullSource.ss -->",
1806
        ),
1807
        array(
1808
            'name' => 'SSViewerTestCommentsFullSourceHTML4Doctype',
1809
            'expected' => ""
1810
                . "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML "
1811
                . "4.01//EN\"\t\t\"http://www.w3.org/TR/html4/strict.dtd\">"
1812
                . "<!-- template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->"
1813
                . "<html>"
1814
                . "\t<head></head>"
1815
                . "\t<body></body>"
1816
                . "</html>"
1817
                . "<!-- end template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->",
1818
        ),
1819
        array(
1820
            'name' => 'SSViewerTestCommentsFullSourceNoDoctype',
1821
            'expected' => ""
1822
                . "<html><!-- template $f/SSViewerTestCommentsFullSourceNoDoctype.ss -->"
1823
                . "\t<head></head>"
1824
                . "\t<body></body>"
1825
                . "<!-- end template $f/SSViewerTestCommentsFullSourceNoDoctype.ss --></html>",
1826
        ),
1827
        array(
1828
            'name' => 'SSViewerTestCommentsFullSourceIfIE',
1829
            'expected' => ""
1830
                . "<!doctype html>"
1831
                . "<!-- template $f/SSViewerTestCommentsFullSourceIfIE.ss -->"
1832
                . "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1833
                . "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1834
                . "<!--[if !IE]><!--> <html class='no-ie'> <!--<![endif]-->"
1835
                . "\t<head></head>"
1836
                . "\t<body></body>"
1837
                . "</html>"
1838
                . "<!-- end template $f/SSViewerTestCommentsFullSourceIfIE.ss -->",
1839
        ),
1840
        array(
1841
            'name' => 'SSViewerTestCommentsFullSourceIfIENoDoctype',
1842
            'expected' => ""
1843
                . "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1844
                . "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1845
                . "<!--[if !IE]><!--> <html class='no-ie'>"
1846
                . "<!-- template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss -->"
1847
                . " <!--<![endif]-->"
1848
                . "\t<head></head>"
1849
                . "\t<body></body>"
1850
                . "<!-- end template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss --></html>",
1851
        ),
1852
        array(
1853
            'name' => 'SSViewerTestCommentsPartialSource',
1854
            'expected' =>
1855
            "<!-- template $f/SSViewerTestCommentsPartialSource.ss -->"
1856
                . "<div class='typography'></div>"
1857
                . "<!-- end template $f/SSViewerTestCommentsPartialSource.ss -->",
1858
        ),
1859
        array(
1860
            'name' => 'SSViewerTestCommentsWithInclude',
1861
            'expected' =>
1862
            "<!-- template $f/SSViewerTestCommentsWithInclude.ss -->"
1863
                . "<div class='typography'>"
1864
                . "<!-- include 'SSViewerTestCommentsInclude' -->"
1865
                . "<!-- template $i/SSViewerTestCommentsInclude.ss -->"
1866
                . "Included"
1867
                . "<!-- end template $i/SSViewerTestCommentsInclude.ss -->"
1868
                . "<!-- end include 'SSViewerTestCommentsInclude' -->"
1869
                . "</div>"
1870
                . "<!-- end template $f/SSViewerTestCommentsWithInclude.ss -->",
1871
        ),
1872
        );
1873
        foreach ($templates as $template) {
1874
            $this->_renderWithSourceFileComments('SSViewerTestComments/'.$template['name'], $template['expected']);
1875
        }
1876
    }
1877
    private function _renderWithSourceFileComments($name, $expected)
1878
    {
1879
        $viewer = new SSViewer(array($name));
1880
        $data = new ArrayData(array());
1881
        $result = $viewer->process($data);
1882
        $expected = str_replace(array("\r", "\n"), '', $expected);
1883
        $result = str_replace(array("\r", "\n"), '', $result);
1884
        $this->assertEquals($result, $expected);
1885
    }
1886
1887
    public function testLoopIteratorIterator()
1888
    {
1889
        $list = new PaginatedList(new ArrayList());
1890
        $viewer = new SSViewer_FromString('<% loop List %>$ID - $FirstName<br /><% end_loop %>');
1891
        $result = $viewer->process(new ArrayData(array('List' => $list)));
1892
        $this->assertEquals($result, '');
1893
    }
1894
1895
    public function testProcessOnlyIncludesRequirementsOnce()
1896
    {
1897
        $template = new SSViewer(array('SSViewerTestProcess'));
1898
        $basePath = $this->getCurrentRelativePath() . '/SSViewerTest';
1899
1900
        $backend = Injector::inst()->create(Requirements_Backend::class);
1901
        $backend->setCombinedFilesEnabled(false);
1902
        $backend->combineFiles(
1903
            'RequirementsTest_ab.css',
1904
            array(
1905
            $basePath . '/css/RequirementsTest_a.css',
1906
            $basePath . '/css/RequirementsTest_b.css'
1907
            )
1908
        );
1909
1910
        Requirements::set_backend($backend);
1911
1912
        $this->assertEquals(1, substr_count($template->process(new ViewableData()), "a.css"));
1913
        $this->assertEquals(1, substr_count($template->process(new ViewableData()), "b.css"));
1914
1915
        // if we disable the requirements then we should get nothing
1916
        $template->includeRequirements(false);
1917
        $this->assertEquals(0, substr_count($template->process(new ViewableData()), "a.css"));
1918
        $this->assertEquals(0, substr_count($template->process(new ViewableData()), "b.css"));
1919
    }
1920
1921
    public function testRequireCallInTemplateInclude()
1922
    {
1923
        //TODO undo skip test on the event that templates ever obtain the ability to reference MODULE_DIR (or something to that effect)
1924
        if (FRAMEWORK_DIR === 'framework') {
1925
            $template = new SSViewer(array('SSViewerTestProcess'));
1926
1927
            Requirements::set_suffix_requirements(false);
1928
1929
            $this->assertEquals(
1930
                1,
1931
                substr_count(
1932
                    $template->process(new ViewableData()),
1933
                    "tests/php/View/SSViewerTest/javascript/RequirementsTest_a.js"
1934
                )
1935
            );
1936
        } else {
1937
            $this->markTestSkipped(
1938
                'Requirement will always fail if the framework dir is not '.
1939
                'named \'framework\', since templates require hard coded paths'
1940
            );
1941
        }
1942
    }
1943
1944
    public function testCallsWithArguments()
1945
    {
1946
        $data = new ArrayData(
1947
            array(
1948
            'Set' => new ArrayList(
1949
                array(
1950
                new SSViewerTest\TestObject("1"),
1951
                new SSViewerTest\TestObject("2"),
1952
                new SSViewerTest\TestObject("3"),
1953
                new SSViewerTest\TestObject("4"),
1954
                new SSViewerTest\TestObject("5"),
1955
                )
1956
            ),
1957
            'Level' => new SSViewerTest\LevelTestData(1),
1958
            'Nest' => array(
1959
            'Level' => new SSViewerTest\LevelTestData(2),
1960
            ),
1961
            )
1962
        );
1963
1964
        $tests = array(
1965
        '$Level.output(1)' => '1-1',
1966
        '$Nest.Level.output($Set.First.Number)' => '2-1',
1967
        '<% with $Set %>$Up.Level.output($First.Number)<% end_with %>' => '1-1',
1968
        '<% with $Set %>$Top.Nest.Level.output($First.Number)<% end_with %>' => '2-1',
1969
        '<% loop $Set %>$Up.Nest.Level.output($Number)<% end_loop %>' => '2-12-22-32-42-5',
1970
        '<% loop $Set %>$Top.Level.output($Number)<% end_loop %>' => '1-11-21-31-41-5',
1971
        '<% with $Nest %>$Level.output($Top.Set.First.Number)<% end_with %>' => '2-1',
1972
        '<% with $Level %>$output($Up.Set.Last.Number)<% end_with %>' => '1-5',
1973
        '<% with $Level.forWith($Set.Last.Number) %>$output("hi")<% end_with %>' => '5-hi',
1974
        '<% loop $Level.forLoop($Set.First.Number) %>$Number<% end_loop %>' => '!0',
1975
        '<% with $Nest %>
1976
				<% with $Level.forWith($Up.Set.First.Number) %>$output("hi")<% end_with %>
1977
			<% end_with %>' => '1-hi',
1978
        '<% with $Nest %>
1979
				<% loop $Level.forLoop($Top.Set.Last.Number) %>$Number<% end_loop %>
1980
			<% end_with %>' => '!0!1!2!3!4',
1981
        );
1982
1983
        foreach ($tests as $template => $expected) {
1984
            $this->assertEquals($expected, trim($this->render($template, $data)));
1985
        }
1986
    }
1987
1988
    public function testRepeatedCallsAreCached()
1989
    {
1990
        $data = new SSViewerTest\CacheTestData();
1991
        $template = '
1992
			<% if $TestWithCall %>
1993
				<% with $TestWithCall %>
1994
					{$Message}
1995
				<% end_with %>
1996
1997
				{$TestWithCall.Message}
1998
			<% end_if %>';
1999
2000
        $this->assertEquals('HiHi', preg_replace('/\s+/', '', $this->render($template, $data)));
2001
        $this->assertEquals(
2002
            1,
2003
            $data->testWithCalls,
2004
            'SSViewerTest_CacheTestData::TestWithCall() should only be called once. Subsequent calls should be cached'
2005
        );
2006
2007
        $data = new SSViewerTest\CacheTestData();
2008
        $template = '
2009
			<% if $TestLoopCall %>
2010
				<% loop $TestLoopCall %>
2011
					{$Message}
2012
				<% end_loop %>
2013
			<% end_if %>';
2014
2015
        $this->assertEquals('OneTwo', preg_replace('/\s+/', '', $this->render($template, $data)));
2016
        $this->assertEquals(
2017
            1,
2018
            $data->testLoopCalls,
2019
            'SSViewerTest_CacheTestData::TestLoopCall() should only be called once. Subsequent calls should be cached'
2020
        );
2021
    }
2022
2023
    public function testClosedBlockExtension()
2024
    {
2025
        $count = 0;
2026
        $parser = new SSTemplateParser();
2027
        $parser->addClosedBlock(
2028
            'test',
2029
            function ($res) use (&$count) {
2030
                $count++;
2031
            }
2032
        );
2033
2034
        $template = new SSViewer_FromString("<% test %><% end_test %>", $parser);
2035
        $template->process(new SSViewerTest\TestFixture());
2036
2037
        $this->assertEquals(1, $count);
2038
    }
2039
2040
    public function testOpenBlockExtension()
2041
    {
2042
        $count = 0;
2043
        $parser = new SSTemplateParser();
2044
        $parser->addOpenBlock(
2045
            'test',
2046
            function ($res) use (&$count) {
2047
                $count++;
2048
            }
2049
        );
2050
2051
        $template = new SSViewer_FromString("<% test %>", $parser);
2052
        $template->process(new SSViewerTest\TestFixture());
2053
2054
        $this->assertEquals(1, $count);
2055
    }
2056
2057
    /**
2058
     * Tests if caching for SSViewer_FromString is working
2059
     */
2060
    public function testFromStringCaching()
2061
    {
2062
        $content = 'Test content';
2063
        $cacheFile = TEMP_PATH . DIRECTORY_SEPARATOR . '.cache.' . sha1($content);
2064
        if (file_exists($cacheFile)) {
2065
            unlink($cacheFile);
2066
        }
2067
2068
        // Test global behaviors
2069
        $this->render($content, null, null);
2070
        $this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
2071
2072
        SSViewer_FromString::config()->update('cache_template', true);
2073
        $this->render($content, null, null);
2074
        $this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
2075
        unlink($cacheFile);
2076
2077
        // Test instance behaviors
2078
        $this->render($content, null, false);
2079
        $this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
2080
2081
        $this->render($content, null, true);
2082
        $this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
2083
        unlink($cacheFile);
2084
    }
2085
}
2086